from gettext import gettext as _
from gi.repository import Adw, Gdk, Gio, GLib, Gtk, GObject

import json
import logging
import os
import re
from typing import Any, Callable, Optional

import iotas.config_manager
import iotas.const as const
from iotas.note import Note
from iotas.webkit_pdf_exporter import WebKitPdfExporter


@Gtk.Template(resource_path="/org/gnome/World/Iotas/ui/export_dialog.ui")
class ExportDialog(Adw.Dialog):
    __gtype_name__ = "ExportDialog"

    _main_stack: Gtk.Stack = Gtk.Template.Child()
    _headerbar_stack: Gtk.Stack = Gtk.Template.Child()
    _format_headerbar: Adw.HeaderBar = Gtk.Template.Child()
    _working_headerbar: Adw.HeaderBar = Gtk.Template.Child()
    _success_headerbar: Adw.HeaderBar = Gtk.Template.Child()
    _failure_headerbar: Adw.HeaderBar = Gtk.Template.Child()
    _format_listbox: Gtk.ListBox = Gtk.Template.Child()
    _cancel_button: Gtk.Button = Gtk.Template.Child()
    _show_button: Gtk.Button = Gtk.Template.Child()
    _working: Gtk.Box = Gtk.Template.Child()
    _success: Gtk.Box = Gtk.Template.Child()
    _success_label: Gtk.Label = Gtk.Template.Child()
    _failure: Gtk.Box = Gtk.Template.Child()
    _failure_label: Gtk.Label = Gtk.Template.Child()

    def __init__(
        self,
        note: Note,
        html_generator,
        webkit_init_fn: Callable[[], None],
    ) -> None:
        super().__init__()
        self.__note = note
        self.__html_generator = html_generator
        self.__webkit_init = webkit_init_fn
        # Any typing for lazy loading
        self.__exporter: Optional[Any] = None
        self.__show_path: Optional[str] = None
        self.__custom_formats: dict[str, dict[str, str]] = {}

        controller = Gtk.EventControllerKey.new()
        controller.connect("key-pressed", self._on_key_pressed)
        self.add_controller(controller)

        GLib.idle_add(self.__setup)

    def __setup(self) -> None:
        self._cancel_button.grab_focus()
        self.__load_extra_formats()

    @Gtk.Template.Callback()
    def _row_activated(self, _listbox: Gtk.ListBox, row: Adw.ButtonRow) -> None:
        format_id = row.get_title().lower()
        if format_id in self.__custom_formats:
            extension = self.__custom_formats[format_id]["extension"]
            format = self.__custom_formats[format_id]["format"]
        else:
            extension = format_id
            format = format_id
        self.__determine_output_location(format, extension)

    @Gtk.Template.Callback()
    def _on_show_clicked(self, _button: Gtk.Button) -> None:
        launcher = Gtk.FileLauncher.new(Gio.File.new_for_uri(f"file://{self.__show_path}"))
        launcher.open_containing_folder()
        self.close()

    @Gtk.Template.Callback()
    def _on_cancel(self, _button: Gtk.Button) -> None:
        self.close()

    def _on_key_pressed(
        self,
        controller: Gtk.EventControllerKey,
        keyval: int,
        keycode: int,
        state: Gdk.ModifierType,
    ) -> bool:
        if self._main_stack.get_visible_child() != self._working:
            if keyval in (Gdk.KEY_Down, Gdk.KEY_KP_Down):
                if self.get_property("focus-widget") == self._cancel_button:
                    row = self._format_listbox.get_row_at_index(0)
                    row.grab_focus()
                    return Gdk.EVENT_STOP

        return Gdk.EVENT_PROPAGATE

    def __on_finished(self, _obj: GObject.Object, out_format: str, show_path: str) -> None:
        self._main_stack.set_visible_child(self._success)
        self._headerbar_stack.set_visible_child(self._success_headerbar)
        # Translators: Description, {} is a format eg. PDF
        self._success_label.set_label(_("Exported to {}").format(out_format.upper()))
        self.__show_path = show_path

        # For some reason if I call grab focus directly from the idle it blocks clean exit
        def focus_show_button():
            self._show_button.grab_focus()

        GLib.idle_add(focus_show_button)

    def __on_failed(self, _obj: GObject.Object, out_format: str, reason: str) -> None:
        self._main_stack.set_visible_child(self._failure)
        self._headerbar_stack.set_visible_child(self._failure_headerbar)
        # Translators: Description, {} is a format eg. PDF
        message = _("Failed to export to {}").format(out_format.upper())
        if reason != "":
            message = message + "\n\n" + reason
        self._failure_label.set_label(message)

    def __determine_output_location(self, out_format: str, file_extension: str) -> None:
        self.__init_exporter()
        assert self.__exporter
        filesystem_writable_path = self.__get_writable_non_container_dir()
        if filesystem_writable_path:
            dialog = Gtk.FileDialog.new()
            dialog.set_initial_folder(Gio.File.new_for_path(filesystem_writable_path))
            filename = self.__exporter.build_default_filename(
                self.__note, out_format, file_extension
            )
            dialog.set_initial_name(filename)
            # Translators: Button
            dialog.set_accept_label(_("Export"))
            window = self.get_parent().get_root()
            dialog.save(
                window,
                None,
                self.__on_save_dialog_finish,
                out_format,
                file_extension,
            )
        else:
            logging.info(
                "Can't access user documents directory, exporting to location inside container"
            )
            self.__export(out_format, file_extension, location=None)

    def __on_save_dialog_finish(
        self, dialog: Gtk.FileDialog, task: Gio.Task, out_format: str, file_extension: str
    ) -> None:
        try:
            file = dialog.save_finish(task)
        except GLib.GError as e:
            logging.warning("Couldn't export note: %s", e.message)
            self.close()
        else:
            path = file.get_path()

            if out_format == "html":
                dir_path = os.path.abspath(os.path.join(path, os.pardir))
            else:
                dir_path = os.path.dirname(path)
            iotas.config_manager.set_last_export_directory(dir_path)

            self.__export(out_format, file_extension, path)

    def __export(self, out_format: str, file_extension: str, location: Optional[str]) -> None:
        assert self.__exporter
        self._main_stack.set_visible_child(self._working)
        self._headerbar_stack.set_visible_child(self._working_headerbar)
        self.__exporter.export(
            self.__note,
            out_format,
            file_extension,
            iotas.config_manager.get_markdown_tex_support(),
            location,
        )

    def __get_writable_non_container_dir(self) -> Optional[str]:
        last_dir = iotas.config_manager.get_last_export_directory()
        if last_dir != "" and self.__dir_exists_writable(last_dir):
            return last_dir
        elif self.__can_write_documents_dir():
            return self.__get_documents_dir()
        else:
            return None

    def __get_documents_dir(self) -> Optional[str]:
        return GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOCUMENTS)

    def __can_write_documents_dir(self) -> bool:
        documents_dir = self.__get_documents_dir()
        if documents_dir is None:
            return False
        else:
            return self.__dir_exists_writable(documents_dir)

    def __dir_exists_writable(self, path: str) -> bool:
        return os.path.exists(path) and os.access(path, os.W_OK)

    def __init_exporter(self) -> None:
        # More lazyloading to quicken startup
        from iotas.exporter import Exporter

        self.__pdf_exporter = WebKitPdfExporter(
            self.__webkit_init(), iotas.config_manager.get_markdown_keep_webkit_process()
        )

        self.__exporter = Exporter(self.__pdf_exporter, self.__html_generator, const.PKGDATADIR)
        self.__exporter.connect("finished", self.__on_finished)
        self.__exporter.connect("failed", self.__on_failed)

    def __load_extra_formats(self) -> bool:
        raw_formats = iotas.config_manager.get_extra_export_formats()
        if raw_formats.strip() == "":
            return False
        try:
            formats_json = json.loads(raw_formats)
        except json.JSONDecodeError as e:
            logging.warning("Failed to load extra formats: %s", e.msg)
            return False

        if type(formats_json) is not list:
            logging.warning("Failed to load extra formats: JSON not being a list")
            return False

        added = False
        for item in formats_json:
            if type(item) is not dict:
                logging.warning("Failed to load extra format: item not being an object")
                continue
            if "pandocOutFormat" not in item:
                logging.warning("Failed to load extra format: missing pandocOutFormat")
                continue
            if "fileExtension" not in item:
                logging.warning("Failed to load extra format: missing fileExtension")
                continue

            out_format = item["pandocOutFormat"]
            extension = item["fileExtension"]
            if type(out_format) is not str or out_format.strip() == "":
                logging.warning("Failed to load extra format: pandocOutFormat is not a string")
                continue
            if type(extension) is not str or extension.strip() == "":
                logging.warning("Failed to load extra format: due to fileExtension is not a string")
                continue

            if re.match(r"^[\w_]+$", out_format) is None:
                logging.warning(
                    "Failed to load extra format: pandocOutFormat contains invalid characters"
                )
                continue
            if re.match(r"^[\w]+$", extension) is None:
                logging.warning(
                    "Failed to load extra format: fileExtension contains invalid characters"
                )
                continue

            logging.debug(f"Adding extra pandoc format {out_format} with extension {extension}")
            button = Adw.ButtonRow()
            button.set_title(extension.upper())
            button.set_end_icon_name("go-next-symbolic")
            self._format_listbox.append(button)

            format_id = out_format.lower()
            self.__custom_formats[format_id] = {"format": out_format, "extension": extension}

            added = True

        return added
