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

from gi.repository import GObject, GLib

import logging
import os
import shutil
from threading import Thread
from typing import Optional

try:
    import pypandoc
except ModuleNotFoundError:
    pypandoc = None

from iotas.attachment_helpers import (
    AttachmentsCopyOutcome,
    copy_note_attachments,
    get_attachment_disk_states,
    get_attachments_dir,
)
from iotas.export_utils import (
    build_default_filename,
    get_container_export_dir,
    sanitise_title_for_filename,
)
from iotas.html_generator import HtmlGenerator
from iotas.markdown_helpers import (
    get_image_attachments_from_note_content,
    get_note_export_content,
    parse_to_tokens,
)
from iotas.note import Note
from iotas.note_exporter import NoteExporter
from iotas.pdf_exporter import PdfExporter


class DefaultNoteExporter(NoteExporter):
    """Note exporter.

    :param PdfExporter pdf_exporter: PDF exporter
    :param HtmlGenerator html_generator: HTML generator
    :param str app_data_path: User data path
    """

    __gsignals__ = {
        # str: single export out path
        "finished": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
        # str: reason
        "failed": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
    }

    def __init__(
        self, pdf_exporter: PdfExporter, html_generator: HtmlGenerator, app_data_path: str
    ) -> None:
        super().__init__()
        self.__pdf_exporter = pdf_exporter
        self.__pdf_exporter.set_callbacks(
            self.__on_pdf_export_finished, self.__on_pdf_export_failed
        )
        self.__html_generator = html_generator
        self.__app_data_path = app_data_path
        self.__allow_missing_images = False

    def export(
        self,
        note: Note,
        out_format: str,
        file_extension: str,
        supporting_tex: bool,
        allow_missing_images: bool = False,
        user_location: Optional[str] = None,
    ) -> None:
        """Export note.

        :param Note note: Note to render
        :param str out_format: Export format
        :param str file_extension: File extension
        :param bool supporting_tex: TeX support
        :param bool allow_missing_images: Whether missing images are treated as a failure
        :param str user_location: User chosen export location, optional
        """
        self.__note = note
        self.__out_format = out_format
        self.__supporting_tex = supporting_tex
        self.__allow_missing_images = allow_missing_images

        if user_location:
            self.__location = user_location
        else:
            # Running with limited permissions in container, export to exports dir inside container
            # with automatic filename
            export_dir = get_container_export_dir()
            filename = build_default_filename(note, out_format, file_extension, add_timestamp=True)
            self.__location = os.path.join(export_dir, filename)
            if not os.path.exists(export_dir):
                try:
                    os.mkdir(export_dir)
                except OSError as e:
                    logging.warning(f"Failed to export {out_format} to {self.__location}: {e}")
                    self.emit("failed", e)
                    return

        if not self.__check_for_missing_attachments():
            return

        if out_format == "pdf":
            self.__export_pdf()
        elif out_format == "md":
            self.__export_md()
        elif out_format == "html":
            self.__export_html()
        else:
            logging.info(f"Asking pandoc to export to {out_format}")
            self.__export_pandoc(out_format)

    def __on_pdf_export_finished(self) -> None:
        GLib.idle_add(self.emit, "finished", self.__location)

    def __on_pdf_export_failed(self, error: str) -> None:
        GLib.idle_add(self.emit, "failed", error)

    def __export_pandoc(self, out_format: str) -> None:
        content = get_note_export_content(self.__note, prefix_note_id=True)
        working_dir = os.path.abspath(os.path.join(get_attachments_dir(), os.pardir))

        def thread_do():
            if pypandoc:
                try:
                    pypandoc.convert_text(
                        content,
                        out_format,
                        format="gfm+attributes+implicit_figures",
                        outputfile=self.__location,
                        cworkdir=working_dir,
                    )
                except (RuntimeError, OSError) as e:
                    logging.warning(f"Failed to export {out_format} to {self.__location}: {e}")
                    GLib.idle_add(self.emit, "failed", str(e))
                else:
                    logging.info(f"Exported {self.__out_format} to {self.__location}")
                    GLib.idle_add(self.emit, "finished", self.__location)
            else:
                logging.warning(
                    f"Failed to export {out_format} to {self.__location}: "
                    "Pypandoc is not installed on your system."
                )
                self.emit("failed", "Pypandoc is not installed on your system.")

        thread = Thread(target=thread_do)
        thread.daemon = True
        thread.start()

    def __export_pdf(self) -> None:
        self.__pdf_exporter.export(self.__note, self.__location)

    def __export_md(self) -> None:

        def thread_do() -> None:
            if len(get_image_attachments_from_note_content(self.__note)) > 0:
                file_location = os.path.join(
                    self.__location, f"{sanitise_title_for_filename(self.__note.title)}.md"
                )
                if not self.__create_export_directory():
                    return
            else:
                file_location = self.__location

            export_content = get_note_export_content(self.__note, prefix_note_id=False)
            try:
                with open(file_location, "w") as f:
                    f.write(export_content)
            except OSError as e:
                GLib.idle_add(self.emit, "failed", e)
                logging.warning(f"Failed to export MD: {e}")
                return

            if not self.__copy_attachments():
                return

            logging.info(f"Exported {self.__out_format} to {file_location}")
            GLib.idle_add(self.emit, "finished", self.__location)

        thread = Thread(target=thread_do)
        thread.daemon = True
        thread.start()

    def __export_html(self) -> None:

        def thread_do() -> None:

            parser, tokens = parse_to_tokens(
                self.__note, exporting=True, tex_support=self.__supporting_tex
            )
            html = self.__html_generator.generate(
                self.__note,
                tokens,
                parser.renderer.render,
                parser.options,
                searching=False,
                export_format="html",
            )

            if not self.__create_export_directory():
                return

            index_filename = os.path.join(self.__location, "index.html")

            try:
                with open(index_filename, "w") as f:
                    f.write(html)
            except OSError as e:
                GLib.idle_add(self.emit, "failed", e)
                logging.warning(f"Failed to export HTML: {e}")
                return

            if not self.__copy_attachments():
                return

            css_dir = os.path.join(self.__location, "css")

            try:
                os.mkdir(css_dir)
            except OSError as e:
                GLib.idle_add(self.emit, "failed", e)
                logging.warning(f"Failed to export HTML: {e}")
                return

            dest_file = os.path.join(css_dir, os.path.basename(HtmlGenerator.RESOURCE_CSS_PATH))

            try:
                shutil.copyfile(
                    f"{self.__app_data_path}/{HtmlGenerator.RESOURCE_CSS_PATH}", dest_file
                )
            except OSError as e:
                GLib.idle_add(self.emit, "failed", e)
                logging.warning(f"Failed to export HTML: {e}")
                return

            if self.__supporting_tex:
                dest_file = os.path.join(
                    css_dir, os.path.basename(HtmlGenerator.RESOURCE_KATEX_CSS_PATH)
                )

                try:
                    shutil.copyfile(
                        f"{self.__app_data_path}/{HtmlGenerator.RESOURCE_KATEX_CSS_PATH}", dest_file
                    )
                except OSError as e:
                    GLib.idle_add(self.emit, "failed", e)
                    logging.warning(f"Failed to export HTML: {e}")
                    return

                js_dir = os.path.join(self.__location, "js")

                try:
                    os.mkdir(js_dir)
                except OSError as e:
                    GLib.idle_add(self.emit, "failed", e)
                    logging.warning(f"Failed to export HTML: {e}")
                    return

                dest_file = os.path.join(
                    js_dir, os.path.basename(HtmlGenerator.RESOURCE_KATEX_JS_PATH)
                )

                try:
                    shutil.copyfile(
                        f"{self.__app_data_path}/{HtmlGenerator.RESOURCE_KATEX_JS_PATH}", dest_file
                    )
                except OSError as e:
                    GLib.idle_add(self.emit, "failed", e)
                    logging.warning(f"Failed to export HTML: {e}")
                    return

            logging.info(f"Exported {self.__out_format} to {self.__location}")
            GLib.idle_add(self.emit, "finished", self.__location)

        thread = Thread(target=thread_do)
        thread.daemon = True
        thread.start()

    def __create_export_directory(self) -> bool:
        if os.path.exists(self.__location):
            logging.debug(f"Removing existing {self.__location}")
            try:
                shutil.rmtree(self.__location)
            except OSError as e:
                self.emit("failed", e)
                logging.warning(f"Failed to replace dir for {self.__out_format} export: {e}")
                return False

        try:
            os.mkdir(self.__location)
        except OSError as e:
            self.emit("failed", e)
            logging.warning(f"Failed to make dir for {self.__out_format} export: {e}")
            return False

        return True

    def __check_for_missing_attachments(self) -> bool:
        if len(get_image_attachments_from_note_content(self.__note)) > 0:
            logging.debug("Checking attachments are on disk")
            parser, tokens = parse_to_tokens(
                self.__note, exporting=True, tex_support=self.__supporting_tex
            )
            states = get_attachment_disk_states(self.__note, tokens)
            success = len(states.missing) == 0
            if not success and not self.__allow_missing_images:
                self.emit("failed", "Failed to export some attachments")
                return False

        return True

    def __copy_attachments(self) -> bool:
        if len(get_image_attachments_from_note_content(self.__note)) > 0:
            attachments_dir = os.path.join(self.__location, "attachments")
            logging.debug("Copying attachments")
            result = copy_note_attachments(self.__note, attachments_dir, prefix_note_id=False)
            success = result.outcome in (
                AttachmentsCopyOutcome.NONE,
                AttachmentsCopyOutcome.SUCCESS,
            )
            if not success and not self.__allow_missing_images:
                self.emit("failed", "Failed to export some attachments")
                return False

        return True
