# SPDX-FileCopyrightText: 2022 Chris Heywood
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Gio

import html
from typing import Callable, NamedTuple, Optional

from markdown_it.token import Token
from markdown_it.utils import EnvType, OptionsDict

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


class TemplateDetails(NamedTuple):
    """Template string parameters."""

    html: str

    javascript: str

    css: str

    search_css: str


class TemplateHtmlGenerator(HtmlGenerator):
    """Generate HTML from note contents using a template."""

    EXPORT_CSS_PATH = "css/markdown.css"
    EXPORT_KATEX_CSS_PATH = "css/katex.min.css"
    EXPORT_KATEX_JS_PATH = "js/katex.min.js"

    TEXT_COLOUR_LIGHT = "#504e55"  # GtkSourceView Dark 3
    TEXT_COLOUR_LIGHT_HIGH_CONTRAST = "#000000"

    TEXT_COLOUR_DARK = "#c0bfbc"  # GtkSourceView Light 5
    TEXT_COLOUR_DARK_HIGH_CONTRAST = "#ffffff"

    CHECKED_TASK_COLOUR_LIGHT = "#b0afac"  # GtkSourceView Light 6
    CHECKED_TASK_COLOUR_LIGHT_HIGH_CONTRAST = "#000000"

    CHECKED_TASK_COLOUR_DARK = "#77767b"  # GtkSourceView Dark 1
    CHECKED_TASK_COLOUR_DARK_HIGH_CONTRAST = "#ffffff"

    def __init__(
        self,
        config_manager: HtmlGeneratorConfiguration,
        app_data_path: str,
        toolbar_underlay_padding_height: int,
        custom_template_loader: Optional[Callable[[], TemplateDetails]] = None,
    ) -> None:
        super().__init__()
        self.__config_manager = config_manager
        self.__app_data_path = app_data_path
        self.__toolbar_underlay_padding_height = toolbar_underlay_padding_height
        self.__font_family = ""
        self.__html_template: Optional[str] = None
        self.__javascript_template: str
        self.__configurable_css_template: str
        self.__searching_css: str
        self.__template_loader = (
            custom_template_loader
            if custom_template_loader
            else self.__populate_template_from_resources
        )

    def generate(
        self,
        note: Note,
        tokens: list[Token],
        render_func: Callable[[list[Token], OptionsDict, EnvType], str],
        parser_options: OptionsDict,
        searching: bool,
        export_format: Optional[str],
        scroll_position: Optional[float] = None,
    ) -> str:
        """Generator HTML for note.

        :param Note note: Note to render
        :param list[Token] tokens: Parser tokens
        :param Callable[[list[Token], OptionsDict, EnvType], str] render_fun: Render function
        :param OptionsDict parser_options: Parser options
        :param bool searching: Whether search CSS should be included
        :param Optional[str] export_format: Export format, if using
        :param Optional[float] scroll_position: Position to scroll to
        :return: Generated HTML
        :rtype: str
        """
        for token in tokens:
            if token.map:
                token.attrs["data-map"] = token.map[0]
        content = render_func(tokens, parser_options, None)

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

        javascript = ""
        exporting = export_format is not None
        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.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

    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.__html_template is None:
            self.__populate_templates()

        font_size = self.__get_font_size()
        line_length = self.__config_manager.line_length
        backup_font = (
            "monospace" if self.__config_manager.markdown_use_monospace_font else "sans-serif"
        )
        families = [self.__font_family, backup_font]
        font_family = ", ".join(families)
        if self.__config_manager.editor_header_bar_visible_for_window_state:
            padding_top = 1
            thin_window_top_padding = "0"
        else:
            padding_top = self.__toolbar_underlay_padding_height
            thin_window_top_padding = "-webkit-calc(42px - 1em)"

        if self.__config_manager.high_contrast:
            light_text_colour = self.TEXT_COLOUR_LIGHT_HIGH_CONTRAST
            dark_text_colour = self.TEXT_COLOUR_DARK_HIGH_CONTRAST
            light_checked_task_colour = self.CHECKED_TASK_COLOUR_LIGHT_HIGH_CONTRAST
            dark_checked_task_colour = self.CHECKED_TASK_COLOUR_DARK_HIGH_CONTRAST
            figcaption_opacity = "1.0"
        else:
            light_text_colour = self.TEXT_COLOUR_LIGHT
            dark_text_colour = self.TEXT_COLOUR_DARK
            light_checked_task_colour = self.CHECKED_TASK_COLOUR_LIGHT
            dark_checked_task_colour = self.CHECKED_TASK_COLOUR_DARK
            figcaption_opacity = "0.85"

        stylesheet = self.__configurable_css_template % (
            light_text_colour,
            figcaption_opacity,
            light_checked_task_colour,
            font_size,
            line_length,
            font_family,
            padding_top,
            thin_window_top_padding,
            dark_text_colour,
            dark_checked_task_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

    def __get_font_size(self) -> float:
        monospace_editor = self.__config_manager.use_monospace_font
        monospace_render = self.__config_manager.markdown_use_monospace_font
        editor_size = self.__config_manager.font_size
        ratio = self.__config_manager.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:
        if self.__html_template is not None:
            return
        details = self.__template_loader()
        self.__html_template = details.html
        self.__javascript_template = details.javascript
        self.__configurable_css_template = details.css
        self.__searching_css = details.search_css

    def __populate_template_from_resources(self) -> TemplateDetails:
        return TemplateDetails(
            self.__load_object_from_resource("template.html"),
            self.__load_object_from_resource("binding.js"),
            self.__load_object_from_resource("configurable.css"),
            self.__load_object_from_resource("searching.css"),
        )

    def __load_object_from_resource(self, filename: str) -> str:
        f = Gio.File.new_for_uri(f"resource:///org/gnome/World/Iotas/render-template/{filename}")
        status, contents, _tag = f.load_contents()
        if not status:
            raise Exception(f"Failed to load resource {filename}")

        return contents.decode("utf-8")
