import gi

gi.require_version('WebKit2', '4.0')
from gi.repository import WebKit2, GLib, GObject


class WebView(WebKit2.WebView):
    """A WebView that provides read/write access to scroll.

    It does so using JavaScript, by continuously monitoring it while loaded.
    The alternative is using a WebExtension and C-bindings (see reference), but that is more
    complicated implementation-wise, as well as build-wise until we start building with Meson.

    Reference: https://github.com/aperezdc/webkit2gtk-python-webextension-example
    """

    SYNC_SCROLL_SCALE_JS = """
scale = {:.16f};
write = {};

// Configure MathJax.
if (typeof hasMathJax === "undefined") {{
    hasMathJax = typeof MathJax !== "undefined";
    if (hasMathJax) {{
        MathJax.Hub.Config({{ messageStyle: "none" }});
    }}
}} 

// Figure out if scrollable and rendered.
e = document.documentElement;
canScroll = e.scrollHeight > e.clientHeight;
wasRendered = typeof isRendered !== "undefined" && isRendered;
isRendered = wasRendered ||
        hasMathJax && MathJax.Hub.queue.running == 0 && MathJax.Hub.queue.pending == 0;

// Write the current scroll if instructed or if it was just rendered.
if (canScroll && (write || isRendered && !wasRendered)) {{
    e.scrollTop = (e.scrollHeight - e.clientHeight) * scale;
}}

// Return the current scroll if scrollable and rendered, or -1.
if (canScroll && isRendered) {{
    e.scrollTop / (e.scrollHeight - e.clientHeight);
}} else {{
    -1;
}}
""".strip()

    __gsignals__ = {
        "scroll-scale-changed": (GObject.SIGNAL_RUN_LAST, None, (float,)),
    }

    def __init__(self):
        super().__init__()

        self.connect("load-changed", self.on_load_changed)
        self.connect("load-failed", self.on_load_failed)
        self.connect("size-allocate", self.on_size_allocate)
        self.connect("destroy", self.on_destroy)

        self.scroll_scale = 0.0

        self.state_loaded = False
        self.state_load_failed = False
        self.state_discard_read = False
        self.state_dirty = False
        self.state_waiting = False

        self.timeout_id = None

    def get_scroll_scale(self):
        return self.scroll_scale

    def set_scroll_scale(self, scale):
        self.state_dirty = scale != self.scroll_scale
        self.scroll_scale = scale
        self.state_loop()

    def on_load_changed(self, _web_view, event):
        self.state_loaded = event >= WebKit2.LoadEvent.COMMITTED and not self.state_load_failed
        self.state_load_failed = False
        self.state_discard_read = event == WebKit2.LoadEvent.STARTED and self.state_waiting
        self.state_dirty = True
        self.state_loop()

    def on_load_failed(self, _web_view, _event):
        self.state_loaded = False
        self.state_load_failed = True
        self.state_loop()

    def on_size_allocate(self, *_):
        self.set_scroll_scale(self.scroll_scale)

    def on_destroy(self, _widget):
        self.state_loaded = False
        self.state_loop()

    def sync_scroll_scale(self, scroll_scale, write):
        self.state_waiting = True
        self.run_javascript(
            self.SYNC_SCROLL_SCALE_JS.format(scroll_scale, "true" if write else "false"),
            None, self.finish_sync_scroll_scale)

    def finish_sync_scroll_scale(self, _web_view, result):
        self.state_waiting = False
        result = self.run_javascript_finish(result)
        self.state_loop(result.get_js_value().to_double())

    def state_loop(self, scroll_scale=None, delay=16):  # 16ms ~ 60hz
        # Remove any pending callbacks
        if self.timeout_id:
            GLib.source_remove(self.timeout_id)
            self.timeout_id = None

        # Set scroll scale if specified, and the state is not dirty
        if not self.state_discard_read and scroll_scale not in (None, -1, self.scroll_scale):
            self.scroll_scale = scroll_scale
            self.emit("scroll-scale-changed", self.scroll_scale)
        else:
            self.state_discard_read = False

        # Handle the current state
        if not self.state_loaded or self.state_load_failed or self.state_waiting:
            return
        elif self.state_dirty or delay == 0:
            self.sync_scroll_scale(self.scroll_scale, self.state_dirty)
            self.state_dirty = False
        else:
            self.timeout_id = GLib.timeout_add(delay, self.state_loop, None, 0)
