import gi

gi.require_version("WebKit", "6.0")
from gi.repository import Gdk, Gio, GLib, GObject, WebKit

import json
import logging
from typing import Optional
from uuid import uuid4
import webbrowser

import markdown_it
from mdit_py_plugins.dollarmath import dollarmath_plugin

import iotas.config_manager
from iotas import const
from markdown_it_modified_tasklists_plugin.mdit_tasklists_with_elements_plugin import (
    tasklists_with_elements_plugin,
)
from markdown_it_img_lazyload_plugin.mdit_img_lazyload_plugin import (
    img_lazyload_plugin,
)


class MarkdownRenderView(WebKit.WebView):
    __gtype_name__ = "MarkdownRenderView"

    __gsignals__ = {
        "checkbox-toggled": (GObject.SignalFlags.RUN_FIRST, None, (int, bool)),
        "loaded": (GObject.SignalFlags.RUN_FIRST, None, ()),
    }

    CSS_PATH = "/media/css/web/markdown.css"
    KATEX_CSS_PATH = "/media/css/web/katex.min.css"
    KATEX_JS_PATH = "/media/js/katex.min.js"

    def __init__(self):
        super().__init__()
        self.__user_stylesheet = None
        self.__scroll_position = 0
        self.__font_family = ""
        self.__engine_initialised = False
        self.__searching = False

    def setup(self) -> None:
        """Setup for new WebKit instance."""
        self.connect("decide-policy", self.on_decide_policy)
        self.connect("load-changed", self.__on_load_changed)
        self.connect("context-menu", self.on_context_click)
        content_manager = self.get_user_content_manager()
        content_manager.register_script_message_handler("toPython")
        content_manager.connect("script-message-received", self.__handle_js_message)
        self.__content_manager = content_manager
        iotas.config_manager.settings.connect(
            f"changed::{iotas.config_manager.FONT_SIZE}", self.__on_style_setting_changed
        )
        iotas.config_manager.settings.connect(
            f"changed::{iotas.config_manager.MARKDOWN_RENDER_MONOSPACE_FONT_RATIO}",
            self.__on_style_setting_changed,
        )
        iotas.config_manager.settings.connect(
            f"changed::{iotas.config_manager.LINE_LENGTH}", self.__on_style_setting_changed
        )
        app = Gio.Application.get_default()
        self.__track_timing = app.debug_session or app.development_mode

        self.connect("web-process-terminated", self.__log_terminated)

        # self.get_settings().set_enable_developer_extras(True)

    def render(
        self, input_md: str, read_only: bool, scroll_position: Optional[float] = None
    ) -> None:
        """Render view with new markdown.

        :param str input_md: Markdown to load
        :param bool read_only: Whether note is read_only
        :param Optional[float] scroll_position: Position to scroll to
        """
        self.__update_style()

        md = (
            markdown_it.MarkdownIt("gfm-like")
            .use(tasklists_with_elements_plugin, enabled=not read_only, div=True)
            .use(img_lazyload_plugin)
            .enable("table")
        )
        if iotas.config_manager.get_markdown_tex_support():
            md.use(dollarmath_plugin, renderer=self.render_tex)
        self.__parser_tokens = md.parse(input_md)
        content = md.renderer.render(self.__parser_tokens, md.options, None)

        scroll_js = ""
        if scroll_position is not None:
            scroll_js = f"""document.documentElement.scrollTop = {scroll_position} *
            (document.documentElement.scrollHeight - document.documentElement.clientHeight);
            """
            self.scroll_position = scroll_position
        else:
            self.scroll_position = 0

        javascript = """
        function checkbox_clicked(el) {
            window.webkit.messageHandlers.toPython.postMessage(
                {type: 'checkbox', id: el.id, checked: el.checked});
        }
        function task_div_clicked(event, id) {
            if (event.target.nodeName === 'A')
                return;
            el = document.getElementById(id);
            el.checked = !el.checked;
            window.webkit.messageHandlers.toPython.postMessage(
                {type: 'checkbox', id: el.id, checked: el.checked});
        }
        function add_checkbox_handlers() {
            const selectors = document.querySelectorAll('input[type=checkbox]');
            for (const el of selectors) {
                el.onclick = function(){checkbox_clicked(el)};
                div = el.nextSibling;
                div.onclick = function(){task_div_clicked(event, el.getAttribute('id'))};
                if (div.innerHTML.startsWith(' '))
                    div.innerHTML = div.innerHTML.trim();
            }
        }
        var scroll_update_queued = false;
        function on_scroll() {
            if (!scroll_update_queued) {
                scroll_update_queued = true;
                setTimeout(send_scroll_position, 250);
            }
        }
        function send_scroll_position() {
            window.webkit.messageHandlers.toPython.postMessage(
                {type: 'scrollPosition', position: get_scroll_position_proportion()});
            scroll_update_queued = false;
        }
        function get_scroll_position_proportion() {
            let el = document.documentElement;
            return el.scrollTop / (el.scrollHeight - el.clientHeight);
        }
        window.onload = function() {
            if (%s)
                add_checkbox_handlers();
            window.addEventListener('scroll', on_scroll);
            %s
        }
        """ % (
            str(not read_only).lower(),
            scroll_js,
        )

        css_path = f"{const.PKGDATADIR}/{self.CSS_PATH}"
        katex_css_path = f"{const.PKGDATADIR}/{self.KATEX_CSS_PATH}"
        katex_js_path = f"{const.PKGDATADIR}/{self.KATEX_JS_PATH}"
        if iotas.config_manager.get_markdown_tex_support():
            tex_headers = f'<link rel="stylesheet" href="{katex_css_path}">\n'
            tex_headers += f'<script src="{katex_js_path}"></script>\n'
        else:
            tex_headers = ""

        content = f"""
        <!DOCTYPE html>
        <html>
        <head>
          <link rel="stylesheet" href="{css_path}">
          {tex_headers}
          <script>{javascript}</script>
        </head>
        <body>{content}</body>
        </html>
        """
        self.__load_started_at = GLib.get_monotonic_time()
        self.load_html(content, "file://localhost/")

    def render_retaining_scroll(self, input_md: str, read_only: bool) -> None:
        """Render view with new markdown retaining scroll position.

        :param str input_md: Markdown to load
        :param bool read_only: Whether note is read_only
        """
        self.render(input_md, read_only, self.scroll_position)

    def update_font(self, font_family: str) -> None:
        """Update the font family used in the render.

        :param str font_family: The font family
        """
        self.__font_family = font_family
        self.__update_style()

    @staticmethod
    def render_tex(eq: str, options: dict) -> str:
        """Render TeX markdown into HTML element.

        :param str eq: Equation
        :param dict options: Render options
        :return: Generated HTML
        :rtype: str
        """
        span_id = f"tex-{uuid4()}"
        eq = eq.replace("\\", "\\\\")
        js_options = f'{{displayMode: {str(options["display_mode"]).lower()}}}'
        katex_cmd = (
            f"var eq = `{eq}`; "
            + f"katex.render(eq, document.getElementById('{span_id}'), {js_options});"
        )
        return f'<span id="{span_id}"><script>{katex_cmd}</script></span>'

    @staticmethod
    def on_decide_policy(
        _web_view: WebKit.WebView,
        decision: WebKit.PolicyDecision,
        decision_type: WebKit.PolicyDecisionType,
    ) -> None:
        if (
            decision_type == WebKit.PolicyDecisionType.NAVIGATION_ACTION
            and decision.get_navigation_action().is_user_gesture()
        ):
            uri = decision.get_navigation_action().get_request().get_uri()
            webbrowser.open(uri)
            decision.ignore()
            return True
        return False

    @staticmethod
    def on_context_click(
        _web_view: WebKit.WebView,
        menu: WebKit.ContextMenu,
        _event: Gdk.Event,
    ) -> None:
        for item in menu.get_items():
            if item.get_stock_action() in [
                WebKit.ContextMenuAction.DOWNLOAD_LINK_TO_DISK,
                WebKit.ContextMenuAction.GO_BACK,
                WebKit.ContextMenuAction.GO_FORWARD,
                WebKit.ContextMenuAction.OPEN_LINK,
                WebKit.ContextMenuAction.OPEN_LINK_IN_NEW_WINDOW,
                WebKit.ContextMenuAction.RELOAD,
                WebKit.ContextMenuAction.STOP,
            ]:
                menu.remove(item)

    @GObject.Property(type=float)
    def scroll_position(self) -> float:
        return self.__scroll_position

    @scroll_position.setter
    def scroll_position(self, value: float) -> None:
        self.__scroll_position = value

    @GObject.Property(type=bool, default=False)
    def searching(self) -> bool:
        return self.__searching

    @searching.setter
    def searching(self, value: bool) -> None:
        self.__searching = value
        self.__update_style()

    def __on_load_changed(self, _web_view: WebKit.WebView, load_event: WebKit.LoadEvent) -> None:
        if load_event == WebKit.LoadEvent.FINISHED:
            if self.__track_timing:
                duration = GLib.get_monotonic_time() - self.__load_started_at
                if self.__engine_initialised:
                    logging.debug(f"Render took {duration/1000:.2f}ms")
                else:
                    logging.debug(f"WebKit initialiation and render took {duration/1000:.2f}ms")
                self.__engine_initialised = True
            self.emit("loaded")

    def __on_style_setting_changed(self, _obj: GObject.Object, _value: GObject.ParamSpec) -> None:
        self.__update_style()

    def __handle_js_message(self, _manager: WebKit.UserContentManager, result) -> None:
        js = result.to_json(2)
        message = json.loads(js)
        if message["type"] == "checkbox":
            self.__handle_checkbox_toggle(message)
        elif message["type"] == "scrollPosition":
            if "position" not in message or message["position"] is None:
                logging.warning("Received scroll position message with no position")
                return
            self.__handle_scroll_position(message)

    def __handle_checkbox_toggle(self, message: dict) -> None:
        search_id = message["id"]
        new_value = message["checked"]

        def match(token: markdown_it.token.Token, search_id: str) -> bool:
            search_str = f'id="{search_id}"'
            return token.type == "html_inline" and search_str in token.content

        def handle_match(line: int, new_value: bool) -> None:
            self.emit("checkbox-toggled", line, new_value)

        line = None
        for token in self.__parser_tokens:
            if token.map is not None:
                line = token.map[0]
            if match(token, search_id):
                logging.debug(
                    f'Setting checkbox on line {line} with id "{search_id}" to {new_value}'
                )
                handle_match(line, new_value)
                return
            if token.children is not None:
                for child in token.children:
                    if match(child, search_id):
                        if token.map is not None:
                            line = token.map[0]
                        logging.debug(
                            f'Setting checkbox on line {line} with id "{search_id}" to {new_value}'
                        )
                        handle_match(line, new_value)
                        return

    def __handle_scroll_position(self, message: dict) -> None:
        self.scroll_position = message["position"]

    def __get_font_size(self) -> float:
        monospace_editor = iotas.config_manager.get_use_monospace_font()
        monospace_render = iotas.config_manager.get_markdown_use_monospace_font()
        editor_size = iotas.config_manager.get_font_size()
        ratio = iotas.config_manager.get_markdown_render_monospace_font_ratio()
        if monospace_editor and not monospace_render:
            size = editor_size / ratio
        elif monospace_render and not monospace_editor:
            size = editor_size * ratio
        else:
            size = editor_size
        return size

    def __update_style(self) -> None:
        new_size = self.__get_font_size()
        new_length = iotas.config_manager.get_line_length()
        backup_font = (
            "monospace" if iotas.config_manager.get_markdown_use_monospace_font() else "sans-serif"
        )
        families = [self.__font_family, backup_font]
        new_font_family = "; ".join(families)

        searching_section = self.__build_searching_css()

        stylesheet = """body {
          font-size: %dpt;
          max-width: %dpx;
          font-family: %s;
        }

        %s""" % (
            new_size,
            new_length,
            new_font_family,
            searching_section,
        )
        if self.__user_stylesheet is not None:
            self.__content_manager.remove_style_sheet(self.__user_stylesheet)

        self.__user_stylesheet = WebKit.UserStyleSheet(
            stylesheet,
            WebKit.UserContentInjectedFrames.ALL_FRAMES,
            WebKit.UserStyleLevel.USER,
            None,
            None,
        )
        self.__content_manager.add_style_sheet(self.__user_stylesheet)

    def __build_searching_css(self) -> str:
        if self.__searching:
            css = """  ::selection {
              color: var(--dark-6);
              background-color: var(--yellow-4);
            }

            @media (prefers-color-scheme: dark) {
              ::selection {
                background-color: var(--yellow-1);
              }
            }
            """
        else:
            css = ""
        return css

    def __log_terminated(
        self, _web_view: WebKit.WebView, reason: WebKit.WebProcessTerminationReason
    ) -> None:
        if self.__track_timing:
            logging.debug(f"WebKit process terminated for {reason}")
        self.__engine_initialised = False
