from gi.repository import Gio, GObject

import html
from typing import Optional, Tuple
from uuid import uuid4

import markdown_it
from mdit_py_plugins.dollarmath import dollarmath_plugin
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,
)

from iotas.html_generator_configuration import HtmlGeneratorConfiguration
from iotas.note import Note
from iotas.text_utils import has_unicode


class HtmlGenerator(GObject.Object):

    RESOURCE_CSS_PATH = "/media/css/web/markdown.css"
    RESOURCE_KATEX_CSS_PATH = "/media/css/web/katex.min.css"
    RESOURCE_KATEX_JS_PATH = "/media/js/katex.min.js"
    EXPORT_CSS_PATH = "css/markdown.css"
    EXPORT_KATEX_CSS_PATH = "css/katex.min.css"
    EXPORT_KATEX_JS_PATH = "js/katex.min.js"

    # Dark 3
    TEXT_COLOUR_LIGHT = "#504e55"
    TEXT_COLOUR_LIGHT_HIGH_CONTRAST = "#000000"
    # Light 5
    TEXT_COLOUR_DARK = "#c0bfbc"
    TEXT_COLOUR_DARK_HIGH_CONTRAST = "#ffffff"

    def __init__(
        self, config_manager: HtmlGeneratorConfiguration, app_data_path: str = None
    ) -> None:
        self.__config_manager = config_manager
        self.__app_data_path = app_data_path
        self.__font_family = ""
        self.__html_template = None
        self.__javascript_template = None
        self.__configurable_css_template = None
        self.__searching_css = None

    def generate(
        self,
        note: Note,
        searching: bool,
        export_format: Optional[str],
        scroll_position: Optional[float] = None,
    ) -> Tuple[str, list]:
        """Generator HTML for note.

        :param Note note: Note to render
        :param bool searching: Whether search CSS should be included
        :param bool export_format: Export format, if using
        :param Optional[float] scroll_position: Position to scroll to
        :return: Generated HTML and list of parser tokens
        :rtype: Tuple[str, list]
        """
        exporting = export_format is not None
        md = (
            markdown_it.MarkdownIt("gfm-like")
            .use(tasklists_with_elements_plugin, enabled=not note.read_only, div=True)
            .enable("table")
        )
        if not exporting:
            md.use(img_lazyload_plugin)
        if self.__config_manager.get_markdown_tex_support():
            md.use(dollarmath_plugin, renderer=self.render_tex)
        parser_tokens = md.parse(note.content)
        for token in parser_tokens:
            if token.map:
                token.attrs["data-map"] = token.map[0]
        content = md.renderer.render(parser_tokens, md.options, None)

        if self.__html_template is None:
            self.__populate_templates()

        javascript = ""
        if not exporting:
            scroll_js = ""
            if scroll_position is not None:
                scroll_js = f"""document.documentElement.scrollTop = {scroll_position} *
                (document.documentElement.scrollHeight - document.documentElement.clientHeight);
                """
            javascript = self.__javascript_template % (str(not note.read_only).lower(), scroll_js)
            javascript = f"\n  <script>{javascript}</script>\n"

        if export_format == "html":
            css_path = self.EXPORT_CSS_PATH
            katex_css_path = self.EXPORT_KATEX_CSS_PATH
            katex_js_path = self.EXPORT_KATEX_JS_PATH
        else:
            css_path = f"{self.__app_data_path}/{self.RESOURCE_CSS_PATH}"
            katex_css_path = f"{self.__app_data_path}/{self.RESOURCE_KATEX_CSS_PATH}"
            katex_js_path = f"{self.__app_data_path}/{self.RESOURCE_KATEX_JS_PATH}"
        if self.__config_manager.get_markdown_tex_support():
            tex_headers = f'\n  <link rel="stylesheet" href="{katex_css_path}">'
            tex_headers += f'\n  <script src="{katex_js_path}"></script>'
        else:
            tex_headers = ""
        if has_unicode(content):
            meta_headers = '\n  <meta charset="utf-8">'
        else:
            meta_headers = ""

        configurable_stylesheet = ""
        if export_format == "html":
            stylesheet = self.generate_user_stylesheet(searching)
            configurable_stylesheet = f"\n  <style>\n{stylesheet}  </style>"

        content = self.__html_template.format(
            title=html.escape(note.title),
            css_path=css_path,
            meta_headers=meta_headers,
            configurable_stylesheet=configurable_stylesheet,
            tex_headers=tex_headers,
            javascript=javascript,
            content=content,
        )
        return (content, parser_tokens)

    def generate_user_stylesheet(self, searching: bool) -> str:
        """Generate part of stylesheet based on state (configuration etc).

        :param bool searching: Whether searching
        :return: stylesheet
        :rtype: str
        """
        if self.__configurable_css_template is None:
            self.__populate_templates()

        new_size = self.__get_font_size()
        new_length = self.__config_manager.get_line_length()
        backup_font = (
            "monospace" if self.__config_manager.get_markdown_use_monospace_font() else "sans-serif"
        )
        families = [self.__font_family, backup_font]
        new_font_family = ", ".join(families)
        if self.__config_manager.get_editor_header_bar_visible_for_window_state():
            new_margin = 1
        else:
            new_margin = self.__config_manager.get_toolbar_underlay_padding_height()

        if self.__config_manager.get_high_contrast():
            light_text_colour = self.TEXT_COLOUR_LIGHT_HIGH_CONTRAST
            dark_text_colour = self.TEXT_COLOUR_DARK_HIGH_CONTRAST
        else:
            light_text_colour = self.TEXT_COLOUR_LIGHT
            dark_text_colour = self.TEXT_COLOUR_DARK

        stylesheet = self.__configurable_css_template % (
            light_text_colour,
            new_size,
            new_length,
            new_font_family,
            new_margin,
            dark_text_colour,
        )

        if searching:
            stylesheet += self.__searching_css

        return stylesheet

    def update_font_family(self, family: str) -> None:
        """Update the font family.

        :param str family: New font family
        """
        self.__font_family = family

    @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>'

    def __get_font_size(self) -> float:
        monospace_editor = self.__config_manager.get_use_monospace_font()
        monospace_render = self.__config_manager.get_markdown_use_monospace_font()
        editor_size = self.__config_manager.get_font_size()
        ratio = self.__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 __populate_templates(self) -> None:
        self.__html_template = self.__load_object_from_resource("template.html")
        self.__javascript_template = self.__load_object_from_resource("binding.js")
        self.__configurable_css_template = self.__load_object_from_resource("configurable.css")
        self.__searching_css = self.__load_object_from_resource("searching.css")

    def __load_object_from_resource(self, filename: str) -> Optional[str]:
        f = Gio.File.new_for_uri(f"resource:///org/gnome/World/Iotas/render-template/{filename}")
        (status, contents, _tag) = f.load_contents()
        if status:
            return contents.decode("utf-8")
        else:
            return None
