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

from gi.repository import GObject, GLib

import logging
import os
import queue
from threading import Thread
from typing import Callable, Optional


from iotas.attachment_helpers import (
    Attachment,
    get_attachment_disk_states,
)
from iotas.export_utils import (
    build_default_filename,
)
from iotas.markdown_helpers import (
    parse_to_tokens,
)
from iotas.note import Note
from iotas.note_exporter import NoteExporter


class BatchExporter(GObject.Object):
    """Batch note exporter.

    :param NoteExporter note_exporter: Single note exporter
    :param Callable[[Attachment], bool] download_attachment: Function to transfer an attachment from
        server, optional
    :param Callable[[Note], None] note_content_populator: Function to populate note content from
        storage
    """

    __gsignals__ = {
        # int: count
        "attachment-transfer-started": (GObject.SignalFlags.RUN_FIRST, None, (int,)),
        # float: progress
        "attachment-transferred": (GObject.SignalFlags.RUN_FIRST, None, (float,)),
        # int: count
        "finished-attachment-detection-transfer-run": (GObject.SignalFlags.RUN_FIRST, None, (int,)),
        "export-started": (GObject.SignalFlags.RUN_FIRST, None, ()),
        # float: progress
        "note-exported": (GObject.SignalFlags.RUN_FIRST, None, (float,)),
        # str: reason
        "export-failed": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
        # str: single export out path
        "batch-finished": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
    }

    def __init__(
        self,
        note_exporter: NoteExporter,
        download_attachment: Optional[Callable[[Attachment], bool]],
        note_content_populator: Optional[Callable[[Note], None]],
    ) -> None:
        super().__init__()
        self.__note_exporter = note_exporter
        self.__download_attachment = download_attachment
        self.__note_content_populator = note_content_populator
        self.__note_exporter.connect("finished", self.__on_single_finished)
        self.__note_exporter.connect("failed", self.__on_single_failed)

        self.__notes: list[Note] = []
        self.__location: Optional[str] = None
        self.__file_extension = ""
        self.__out_format = ""
        self.__supporting_tex = False

        self.__attachments_downloaded = 0
        self.__failed_attachment_transfers: set[int] = set()

        self.__export_queue: queue.Queue[Note] = queue.Queue()

    def check_and_transfer_missing_attachments(self) -> None:
        """Check for any missing attachments and transfer if necessary."""
        if not self.__download_attachment:
            raise Exception("Incorrect usage")

        def thread_do():
            self.__missing_attachments = {}

            for note in self.__notes:
                if not note.content_loaded:
                    self.__note_content_populator(note)

            for note in self.__notes:
                _parser, tokens = parse_to_tokens(note, exporting=False, tex_support=False)
                states = get_attachment_disk_states(note, tokens)
                self.__missing_attachments.update(states.missing)

            logging.debug(f"Have {len(self.__missing_attachments)} missing attachment/s")

            GLib.idle_add(self.__transfer_missing_attachments)

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

    def start_export(self) -> None:
        """Kick off export."""
        assert self.__location  # mypy

        self.emit("export-started")

        if self.multi_export:
            try:
                os.makedirs(self.__location, exist_ok=True)
            except OSError as e:
                logging.warning(f"Failed to create directory: {e}")
                return

        self.__continue_export()

    def cleanup(self) -> None:
        """Flush export queue and disconnect signals."""
        self.__flush_export_queue()
        self.__note_exporter.disconnect_by_func(self.__on_single_finished)
        self.__note_exporter.disconnect_by_func(self.__on_single_failed)

    def get_notes(self) -> list[Note]:
        return self.__notes

    def set_notes(self, notes: list[Note]) -> None:
        self.__notes = notes
        self.__flush_export_queue()
        for note in self.__notes:
            self.__export_queue.put(note)

    @GObject.Property(type=str)
    def location(self) -> Optional[str]:
        return self.__location

    @location.setter
    def set_location(self, value: str) -> None:
        self.__location = value

    @GObject.Property(type=str)
    def out_format(self) -> str:
        return self.__out_format

    @out_format.setter
    def set_out_format(self, value: str) -> None:
        self.__out_format = value

    @GObject.Property(type=str)
    def file_extension(self) -> str:
        return self.__file_extension

    @file_extension.setter
    def set_file_extension(self, value: str) -> None:
        self.__file_extension = value

    @GObject.Property(type=bool, default=False)
    def supporting_tex(self) -> bool:
        return self.__supporting_tex

    @supporting_tex.setter
    def set_supporting_tex(self, value: bool) -> None:
        self.__supporting_tex = value

    @GObject.Property(type=bool, default=False)
    def multi_export(self) -> bool:
        return len(self.__notes) > 1

    def __on_single_finished(self, _obj: GObject.Object, path: str) -> None:
        remaining = self.__export_queue.qsize()
        progress = (len(self.__notes) - remaining) / len(self.__notes)
        self.emit("note-exported", progress)

        if not self.__continue_export():
            if self.multi_export:
                path = os.path.abspath(os.path.join(path, os.pardir))
            self.emit("batch-finished", path)

    def __on_single_failed(self, _obj: GObject.Object, reason: str) -> None:
        self.emit("export-failed", reason)

    def __transfer_missing_attachments(self) -> None:
        count = len(self.__missing_attachments)
        if count == 0:
            self.emit("finished-attachment-detection-transfer-run", 0)
            return

        self.__attachments_downloaded = 0

        logging.info("Downloading attachments prior to export")
        self.emit("attachment-transfer-started", count)

        self.__failed_attachment_transfers = set()

        def thread_do():
            failures = []

            for attachment in self.__missing_attachments.values():
                if self.__download_attachment(attachment):
                    self.__attachments_downloaded += 1
                    progress = self.__attachments_downloaded / len(self.__missing_attachments)
                    GLib.idle_add(self.emit, "attachment-transferred", progress)
                else:
                    failures.append(attachment)

            self.__failed_attachment_transfers = {attachment.note_id for attachment in failures}
            GLib.idle_add(self.emit, "finished-attachment-detection-transfer-run", len(failures))

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

    def __continue_export(self) -> bool:
        try:
            note = self.__export_queue.get_nowait()
        except queue.Empty:
            return False

        missing_attachments = note.id in self.__failed_attachment_transfers
        location: str | None
        if self.multi_export:
            assert self.__location  # We ensure location is set for multi-export

            # For the local backend note contents are populated here instead of during attachment
            # download checking. The contents need to be populated prior to filename building.
            self.__ensure_content_loaded(note)

            filename = build_default_filename(note, self.__out_format, self.__file_extension)
            location = os.path.join(self.__location, filename)
        else:
            location = self.__location

        self.__note_exporter.export(
            note,
            self.__out_format,
            self.__file_extension,
            self.__supporting_tex,
            missing_attachments,
            location,
        )
        return True

    def __ensure_content_loaded(self, note: Note) -> None:
        if not note.content_loaded:
            # The populator won't be provided if exporting from the editor, but in that case
            # the content will always be populated
            assert self.__note_content_populator
            self.__note_content_populator(note)

    def __flush_export_queue(self) -> None:
        while not self.__export_queue.empty():
            try:
                self.__export_queue.get_nowait()
            except queue.Empty:
                pass
