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

from gi.repository import Adw, Gio, GLib, GObject, Gtk

import logging
from typing import Optional

from iotas.attachment import Attachment
from iotas.attachment_helpers import trim_orphaned_images
from iotas.config_manager import ConfigManager
from iotas.category_manager import CategoryManager
import iotas.const as const
from iotas.editor import Editor, EditorSessionDetails
from iotas.export_dialog import ExportDialog
from iotas.index import Index
from iotas.note import Note
from iotas.note_database import NoteDatabase
from iotas.note_manager import NoteManager
from iotas.nextcloud_login_dialog import NextcloudLoginDialog, LoginDialogMode
from iotas.sync_manager import SyncManager
from iotas.uri_to_note_creator import UriToNoteCreator


@Gtk.Template(resource_path="/org/gnome/World/Iotas/ui/window.ui")
class Window(Adw.ApplicationWindow):
    __gtype_name__ = "Window"

    _navigation: Adw.NavigationView = Gtk.Template.Child()
    _index: Index = Gtk.Template.Child()
    _editor: Editor = Gtk.Template.Child()
    _editor_page: Adw.NavigationPage = Gtk.Template.Child()

    def __init__(
        self,
        app: Adw.Application,
        db: NoteDatabase,
        sync_manager: SyncManager,
    ) -> None:
        super().__init__(application=app)

        self.set_icon_name(const.APP_ID)

        if const.IS_DEVEL:
            self.add_css_class("devel")

        self.app = self.get_application()

        self.__db = db
        self.__sync_manager = sync_manager

        self.__category_manager = CategoryManager(self.__db)
        self.__note_manager = NoteManager(self.__db, self.__category_manager)
        self.__config_manager = ConfigManager.get_default()

        self.__initialised = False
        self.__using_keyboard_navigation = False
        self.__secret_service_failed = False
        self.__open_note_after_init = None
        self.__create_note_after_init = False

        self.__setup_actions()

        self._index.setup(self.__note_manager, self.__category_manager, self.__sync_manager)
        self._index.connect(
            "note-opened", lambda _o, note, immediate: self.__open_note(note, immediate)
        )
        self._index.connect("reauthenticate", lambda _o: self.show_login_dialog())
        self._index.connect("export", lambda _o: self.__export_notes())
        self._index.connect(
            "header-bar-height", lambda _o, height: self._editor.set_header_bar_height(height)
        )

        self._editor.setup(self.__category_manager.tree_store, self.__sync_manager)
        self._editor.connect(
            "note-modified",
            lambda _o, note: self.__note_manager.persist_note_while_editing(
                note, update_excerpt=False
            ),
        )
        self._editor.connect("note-deleted", self.__on_editor_note_deleted)
        self._editor.connect(
            "category-changed",
            lambda _o, note, old_category: self.__note_manager.persist_note_category(
                note, old_category
            ),
        )
        self._editor.connect("exit", lambda _o: self._navigation.pop())

        self.__note_manager.connect("sync-requested", lambda _o: self.__sync_manager.sync_now())

        self.__sync_manager.connect("ready", self.__on_sync_ready)
        self.__sync_manager.connect("note-conflict", lambda _o, note: self.__on_note_conflict(note))
        self.__sync_manager.connect(
            "note-deleted", lambda _o, note: self.__on_note_remotely_deleted(note)
        )
        self.__sync_manager.connect("secret-service-failure", self.__on_secret_service_failure)
        self.__sync_manager.connect(
            "notify::authenticated",
            lambda _o, _v: self._editor.set_sync_authenticated(self.__sync_manager.authenticated),
        )
        self.__sync_manager.connect(
            "remote-category-sanitised",
            lambda _o, old, new: self.__category_manager.note_category_changed(old, new),
        )
        self.__sync_manager.connect("attachment-downloaded", self.__on_attachment_downloaded)

        self.__note_manager.connect(
            "initial-load-complete", lambda _o: self.__on_initial_load_from_db_complete()
        )

        self.connect("close-request", lambda _o: self.cleanup_and_close())
        self.connect("notify::visible-dialog", lambda _o, _v: self.__update_for_visible_dialogs())

        width, height = self.__config_manager.get_window_size()
        self.set_default_size(width, height)
        self._index.update_layout_for_initial_size(width, height)
        self.connect(
            "notify::default-height",
            lambda _o, _v: self._index.update_search_pagesize_for_height(self.get_height()),
        )

        self._index.active = True
        self._editor.active = False

    def cleanup_and_close(self) -> None:
        """Cleanup for exit."""
        if self._editor.editing:
            self.__note_manager.apply_any_server_sanitised_title(self._editor.current_note)
            self.__note_manager.persist_note_sync(
                self._editor.current_note, force_update_excerpt=self._editor.note_had_edit
            )
            trim_orphaned_images(self._editor.current_note, on_thread=False)

        self.__note_manager.apply_deleted_notes_attachment_deletion()
        self.__sync_manager.close()
        if not self.is_fullscreen():
            self.__config_manager.set_window_size(self.get_width(), self.get_height())
        self.__config_manager.first_start = False
        self.close()

    def show_login_dialog(self) -> None:
        """Show Nextcloud Notes onboarding dialog."""
        if self.__secret_service_failed:
            mode = LoginDialogMode.SECRET_SERVICE_FAILURE
        elif self.__sync_manager.configured_but_no_password:
            mode = LoginDialogMode.REAUTHENTICATE
        else:
            mode = LoginDialogMode.NORMAL
        dialog = NextcloudLoginDialog(self.__sync_manager, mode)
        dialog.present(self)

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

    @using_keyboard_navigation.setter
    def set_using_keyboard_navigation(self, value: bool) -> None:
        self.__using_keyboard_navigation = value

    @Gtk.Template.Callback()
    def _on_page_pushed(self, _obj: GObject.Object) -> None:
        self._index.active = False
        self._editor.active = True
        self._editor.focus_textview_if_editing()

    @Gtk.Template.Callback()
    def _on_page_popped(self, _obj: GObject.Object, _page: Adw.NavigationPage) -> None:
        note = self._editor.current_note
        self.__note_manager.persist_note_sync(note, force_update_excerpt=self._editor.note_had_edit)
        self._index.refresh_after_note_closed(note)
        self._editor.close_note()
        on_thread = True
        GLib.timeout_add(500, trim_orphaned_images, note, on_thread)
        if note.dirty:
            self.__sync_manager.sync_now()
        self._index.active = True
        self._editor.active = False

    def __setup_actions(self) -> None:
        action_group = Gio.SimpleActionGroup.new()
        app = Gio.Application.get_default()

        action = Gio.SimpleAction.new("fullscreen")
        action.connect("activate", lambda _a, _p: self.__toggle_fullscreen())
        action_group.add_action(action)
        app.set_accels_for_action("win.fullscreen", ["F11"])

        action = Gio.SimpleAction.new("close")
        action.connect("activate", lambda _o, _v: self.cleanup_and_close())
        action_group.add_action(action)
        app.set_accels_for_action("win.close", ["<Control>w"])
        self.__close_window_action = action

        action = Gio.SimpleAction.new("refresh")
        action.connect("activate", lambda _o, _v: self.__sync_manager.sync_now())
        action_group.add_action(action)
        app.set_accels_for_action("win.refresh", ["<Control>r"])
        action.set_enabled(False)

        action = Gio.SimpleAction.new("start-nextcloud-signin")
        action.connect("activate", lambda _o, _v: self.show_login_dialog())
        action_group.add_action(action)

        action = Gio.SimpleAction.new("create-note-from-cli")
        action.connect("activate", lambda _o, _v: self.__on_create_note())
        action_group.add_action(action)

        action = Gio.SimpleAction.new("create-note-from-uri", GLib.VariantType("s"))
        action.connect("activate", self.__on_create_note_from_uri)
        action_group.add_action(action)

        action = Gio.SimpleAction.new("open-note", GLib.VariantType("u"))
        action.connect("activate", self.__on_open_note)
        action_group.add_action(action)

        action = Gio.SimpleAction.new("open-previous-note")
        action.connect("activate", lambda _o, _v: self.__open_previous_note())
        app.set_accels_for_action("win.open-previous-note", ["<Control>l"])
        action_group.add_action(action)

        action = Gio.SimpleAction.new("search-from-cli", GLib.VariantType("s"))
        action.connect("activate", self.__on_search_from_cli)
        action_group.add_action(action)

        self.insert_action_group("win", action_group)
        self.__action_group = action_group

    def __on_editor_note_deleted(self, _obj: GObject.Object, note: Note) -> None:
        if self._navigation.get_visible_page() is not self._editor_page:
            return

        self._navigation.pop()
        self._index.delete_notes([note])

    def __on_note_conflict(self, note: Note) -> None:
        if self._editor.current_note is not None and note is self._editor.current_note:
            logging.info("Note being edited in conflict")
            self._editor.current_note.handling_conflict = True
            self._navigation.pop()
            self._index.show_note_conflict_alert()

    def __on_note_remotely_deleted(self, note: Note) -> None:
        if self._editor.current_note is not None and note is self._editor.current_note:
            logging.info("Note being edited was remotely deleted")
            self._editor.current_note.handling_conflict = True
            self._navigation.pop()
            self._index.show_editing_note_deleted_alert()

    def __on_sync_ready(self, _obj: GObject.Object, new_setup: bool) -> None:
        action = self.__action_group.lookup("refresh")
        action.set_enabled(True)
        action = self.__action_group.lookup("start-nextcloud-signin")
        action.set_enabled(False)

    def __toggle_fullscreen(self) -> None:
        if self.is_fullscreen():
            self.unfullscreen()
        else:
            self.fullscreen()

    def __on_secret_service_failure(self, _obj: GObject.Object) -> None:
        self.__secret_service_failed = True
        if self.__config_manager.show_startup_secret_service_failure:
            self._index.show_secret_service_failure_alert()

    def __on_create_note(self) -> None:
        """Create a new note and edit it."""
        if self.__initialised:
            if self._editor.active:
                self._navigation.pop()
            self.activate_action("index.create-note")
        else:
            self.__create_note_after_init = True

    def __on_open_note(
        self,
        _obj: GObject.Object,
        note_id: GObject.ParamSpec,
    ) -> None:
        note_id = note_id.get_uint32()
        if self.__initialised:
            if self._editor.active:
                self._navigation.pop()
            self.__open_note_by_id(note_id, immediate=False)
        else:
            self.__open_note_after_init = note_id

    # Note: Iotas' addition of this URI handler is a stopgap measure until the freedesktop
    # intent system is fleshed out. The handling provided here will be unceremoniously removed
    # when that system is available and may be removed or modified without notice beforehand;
    # this is not a stable public interface.
    def __on_create_note_from_uri(
        self,
        _obj: GObject.Object,
        param: GObject.ParamSpec,
    ) -> None:
        uri = param.get_string()

        parser = UriToNoteCreator()
        result = parser.parse(uri)
        if result.success:
            assert result.note
            note = result.note
            logging.info(f"Creating note from URI '{note.title}'")
            self.__note_manager.persist_note_sync(note, force_update_excerpt=False)
            self.__sync_manager.sync_now()

            if result.open:
                if self._editor.active:
                    self._navigation.pop()
                self.__open_note(note, immediate=True)

    def __on_search_from_cli(
        self,
        _obj: GObject.Object,
        param: GObject.ParamSpec,
    ) -> None:
        search_term = param.get_string()
        if self._editor.active:
            self._navigation.pop()
        self._index.search_from_cli(search_term)

    def __on_initial_load_from_db_complete(self) -> None:
        self.__initialised = True
        if self.__create_note_after_init:
            note = self.__note_manager.create_note(self.__category_manager.all_category)
            self.__open_note(note, immediate=True)
        elif self.__open_note_after_init is not None:
            self.__open_note_by_id(self.__open_note_after_init, immediate=True)

    def __on_attachment_downloaded(self, _obj: GObject.Object, attachment: Attachment) -> None:
        if not self._editor.active or not self._editor.editing:
            return
        self._editor.show_downloaded_attachment_in_render(attachment)

    def __open_note(
        self, note: Note, immediate: bool, restore_session: Optional[EditorSessionDetails] = None
    ) -> None:
        if immediate:
            self._navigation.set_animate_transitions(False)
        if not note.content_loaded:
            self.__db.populate_note_content(note)
        # Handle opening a note from CLI / shell search provider when another note is open
        if self._editor.active:
            self._navigation.pop()
        self._editor.init_note(note, restore_session)
        self._navigation.push(self._editor_page)
        if immediate:
            self._navigation.set_animate_transitions(True)

    def __open_note_by_id(self, note_id: int, immediate: bool) -> None:
        note = self.__note_manager.fetch_note_by_id(note_id)
        if note is None:
            logging.error(f"Failed to find note with id {note_id}")
            return
        self.__open_note(note, immediate)

    def __open_previous_note(self) -> None:
        sessions = self._editor.get_previous_sessions()
        for session in reversed(sessions):
            if session is None:
                continue
            if not self.__note_manager.fetch_note_by_id(session.note.id):
                continue
            if session.note.locally_deleted:
                continue
            if session.note in self.__note_manager.get_deleted_notes_pending_undo():
                continue
            if self._editor.current_note == session.note:
                continue

            logging.info("Opening previous note")
            session.direct_switching = self._editor.active
            self.__open_note(session.note, immediate=self._editor.active, restore_session=session)
            break

    def __update_for_visible_dialogs(self) -> None:
        visible = self.get_visible_dialog() is not None
        self._index.update_for_dialog_visibility(visible)
        self._editor.update_for_dialog_visibility(visible)
        self.__close_window_action.set_enabled(not visible)

    def __export_notes(self) -> None:
        html_generator = self._editor.ensure_html_generator_initialised()

        notes = self._index.get_selection()
        if not notes:
            return

        dialog = ExportDialog(
            notes,
            html_generator,
            self.__sync_manager.download_attachment if self.__sync_manager.authenticated else None,
            self._editor.ensure_webkit_initialised,
            self.__db.populate_note_content,
        )
        dialog.present(self)
