import gi

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

import json
import logging
from typing import Optional
import webbrowser

import markdown_it

import iotas.config_manager
from iotas.html_generator import HtmlGenerator
from iotas.note import Note


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

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

    def __init__(self) -> None:
        super().__init__()
        self.__user_stylesheet = None
        self.__scroll_position = 0
        self.__engine_initialised = False
        self.__searching = False
        self.__exporting = False

    def setup(self, html_generator: HtmlGenerator) -> None:
        """Setup for new WebKit instance."""
        self.__html_generator = html_generator
        self.connect("decide-policy", self.on_decide_policy)
        self.connect("load-changed", self.__on_load_changed)
        self.connect("load-failed", lambda _v, _e, _u, _f: self.__clear_exporting())
        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}", lambda _o, _v: self.update_style()
        )
        iotas.config_manager.settings.connect(
            f"changed::{iotas.config_manager.MARKDOWN_RENDER_MONOSPACE_FONT_RATIO}",
            lambda _o, _v: self.update_style(),
        )
        iotas.config_manager.settings.connect(
            f"changed::{iotas.config_manager.LINE_LENGTH}", lambda _o, _v: self.update_style()
        )
        iotas.config_manager.settings.connect(
            f"changed::{iotas.config_manager.EDITOR_HEADER_BAR_VISIBILTY}",
            lambda _o, _k: self.update_style(),
        )
        iotas.config_manager.settings.connect(
            f"changed::{iotas.config_manager.EDITOR_FORMATTING_BAR_VISIBILTY}",
            lambda _o, _k: self.update_style(),
        )

        style_manager = Adw.StyleManager.get_default()
        style_manager.connect("notify::high-contrast", lambda _o, _v: self.update_style())

        app = Gio.Application.get_default()
        window = app.get_active_window()
        window.connect("notify::fullscreened", lambda _o, _v: self.update_style())
        self.__track_timing = app.debug_session or app.development_mode

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

        # self.get_settings().set_enable_developer_extras(True)

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

        :param Note note: Note to render
        :param bool export_format: Export format if exporting or None
        """
        self.update_style()
        if export_format is not None:
            self.exporting = True
        (content, self.__parser_tokens) = self.__html_generator.generate(
            note, self.__searching, export_format, scroll_position
        )
        self.scroll_position = scroll_position if scroll_position else 0
        self.__load_started_at = GLib.get_monotonic_time()
        self.load_html(content, "file://localhost/")

    def render_retaining_scroll(self, note: Note, export_format: Optional[str]) -> None:
        """Render view with new markdown retaining scroll position.

        :param Note note: Note to render
        :param bool export_format: Export format if exporting or None
        """
        self.render(note, export_format, self.scroll_position)

    def update_style(self) -> None:
        """Update the configurable style used in the render."""
        stylesheet = self.__html_generator.generate_user_stylesheet(self.__searching)
        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 scroll_to_heading(self, line_number: int) -> None:
        """Scroll to heading.

        :param int line_number: Line the heading in source markdown
        """
        script = f"""
        element = document.querySelector('[data-map="{line_number}"]');
        if (element)
            element.scrollIntoView();
        """
        self.evaluate_javascript(
            script, length=-1, world_name=None, source_uri=None, cancellable=None, callback=None
        )

    @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=bool, default=False)
    def exporting(self) -> bool:
        return self.__exporting

    @exporting.setter
    def exporting(self, value: bool) -> None:
        self.__exporting = value

    @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 initialisation and render took {duration/1000:.2f}ms")
                self.__engine_initialised = True
            self.emit("loaded")

    def __clear_exporting(self) -> None:
        self.exporting = False

    def __handle_js_message(
        self, _manager: WebKit.UserContentManager, result: JavaScriptCore.Value
    ) -> 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.scroll_position = message["position"]

    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 __log_termination(
        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
