#!/usr/bin/python3
import gi

gi.require_version("Gdk", "4.0")
gi.require_version("Adw", "1")

from gi.repository import Gdk, Gio, GLib

import logging

import iotas.const as const
from iotas.database import Database, DbCursor
from iotas.note_database import NoteDatabase


class Server:

    def __init__(self, conn: Gio.DBusConnection, node_info:str, path: str):
        logging.basicConfig(
            format="%(asctime)s | %(module)s | %(levelname)s | %(message)s",
            datefmt="%H:%M:%S",
            level=logging.INFO,
        )

        out_args = {}
        in_args = {}
        for interface in Gio.DBusNodeInfo.new_for_xml(node_info).interfaces:
            for method in interface.methods:
                out_args[method.name] = (
                    "(" + "".join([arg.signature for arg in method.out_args]) + ")"
                )
                in_args[method.name] = tuple(arg.signature for arg in method.in_args)

            conn.register_object(
                object_path=path,
                interface_info=interface,
                method_call_closure=self.__on_method_call,
            )

        self.__method_in_args = in_args
        self.__method_out_args = out_args

    def __on_method_call(
        self,
        _conn: Gio.DBusConnection,
        _sender: str,
        _object_path: str,
        _interface_name: str,
        method_name: str,
        parameters: GLib.Variant,
        invocation: Gio.DBusMethodInvocation,
    ):
        args = list(parameters.unpack())
        for i, sig in enumerate(self.__method_in_args[method_name]):
            if sig == "h":
                msg = invocation.get_message()
                fd_list = msg.get_unix_fd_list()
                args[i] = fd_list.get(args[i])

        try:
            result = getattr(self, method_name)(*args)

            # out_args is at least (signature1).
            # We therefore always wrap the result as a tuple.
            # Refer to https://bugzilla.gnome.org/show_bug.cgi?id=765603
            result = (result,)

            out_args = self.__method_out_args[method_name]
            if out_args != "()":
                variant = GLib.Variant(out_args, result)
                invocation.return_value(variant)
            else:
                invocation.return_value(None)
        except Exception as e:
            logging.warning(f"__on_method_call: {e}")


class SearchIotasService(Server, Gio.Application):
    DBUS_NODE_INFO = """
    <!DOCTYPE node PUBLIC
    '-//freedesktop//DTD D-BUS Object Introspection 1.0//EN'
    'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd'>
    <node>
    <interface name="org.gnome.Shell.SearchProvider2">

    <method name="GetInitialResultSet">
      <arg type="as" name="terms" direction="in" />
      <arg type="as" name="results" direction="out" />
    </method>

    <method name="GetSubsearchResultSet">
      <arg type="as" name="previous_results" direction="in" />
      <arg type="as" name="terms" direction="in" />
      <arg type="as" name="results" direction="out" />
    </method>

    <method name="GetResultMetas">
      <arg type="as" name="identifiers" direction="in" />
      <arg type="aa{sv}" name="metas" direction="out" />
    </method>

    <method name="ActivateResult">
      <arg type="s" name="identifier" direction="in" />
      <arg type="as" name="terms" direction="in" />
      <arg type="u" name="timestamp" direction="in" />
    </method>

    <method name="LaunchSearch">
      <arg type="as" name="terms" direction="in" />
      <arg type="u" name="timestamp" direction="in" />
    </method>

    </interface>
    </node>
    """

    SEARCH_BUS = "org.gnome.Shell.SearchProvider2"
    PATH_BUS = "/org/gnome/World/IotasSearchProvider"
    MAX_SUBSEARCH_LIST_LENGTH = 999

    def __init__(self):
        Gio.Application.__init__(
            self,
            application_id="org.gnome.World.Iotas.SearchProvider",
            flags=Gio.ApplicationFlags.IS_SERVICE,
            inactivity_timeout=10000,
        )
        self.cursors: dict[str, DbCursor] = {}
        self.__db_base = Database()
        self.__db = NoteDatabase(self.__db_base)

        self.__bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
        Gio.bus_own_name_on_connection(
            self.__bus, self.SEARCH_BUS, Gio.BusNameOwnerFlags.NONE, None, None
        )
        Server.__init__(self, self.__bus, self.DBUS_NODE_INFO, self.PATH_BUS)

    def ActivateResult(self, search_id: str, _terms: list[str], timestamp: int) -> None:
        """Activate individual search result.

        :param str search_id: The note id
        :param list[str] _terms: The search terms
        :param int timestamp: Search timestamp
        """
        self.hold()

        launch_context = Gdk.Display.get_app_launch_context(Gdk.Display.get_default())
        launch_context.set_timestamp(timestamp)
        app = Gio.AppInfo.create_from_commandline(
            f"iotas --open-note {search_id}",
            const.APP_ID,
            Gio.AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION,
        )
        if not app:
            logging.error("Failed to create AppInfo")
        elif not app.launch(None, launch_context):
            logging.error("Failed to launch")

        self.release()

    def GetInitialResultSet(self, terms: list[str]) -> list[str]:
        """Search for initial results.

        :param list[str] terms: The search terms
        :return: A list of note ids
        :rtype: list[str]
        """
        self.hold()
        results = []
        try:
            results = self.__search(terms)
        except Exception as e:
            logging.warning(f"GetInitialResultSet: {e}")
        self.release()
        return results

    def GetResultMetas(self, ids: list[str]) -> list[dict]:
        """Fetch metadata for provided note ids.

        :param list[str] ids: Note ids
        :return: List of metadata for provided notes
        :rtype: list[dict]
        """
        self.hold()
        results = []
        gicon = Gio.ThemedIcon.new("text-x-generic").to_string()
        try:
            notes = self.__db.get_notes_by_ids([int(x) for x in ids])
            for note in notes:
                name = note.title
                description = note.excerpt
                d = {
                    "id": GLib.Variant("s", str(note.id)),
                    "description": GLib.Variant("s", GLib.markup_escape_text(description)),
                    "name": GLib.Variant("s", name),
                    "gicon": GLib.Variant("s", gicon),
                }
                results.append(d)
        except Exception as e:
            logging.warning(f"GetResultMetas: {e}")
        self.release()
        return results

    def GetSubsearchResultSet(self, previous_results: list[str], new_terms: list[str]) -> list[str]:
        """Search refining results

        :param list[str] previous_results: Note ids from parent set
        :param list[str] new_terms: The search terms
        :return: A list of note ids
        :rtype: list[str]
        """
        self.hold()
        results = []
        try:
            if len(previous_results) < self.MAX_SUBSEARCH_LIST_LENGTH:
                ids = [int(x) for x in previous_results]
            else:
                ids = []

            results = self.__search(new_terms, ids)
        except Exception as e:
            logging.warning(f"GetSubsearchResultSet: {e}")
        self.release()
        return results

    def LaunchSearch(self, terms: list[str], timestamp: int) -> None:
        """Search in app for the provided terms.

        :param list[str] terms: The search terms
        :param int timestamp: Search timestamp
        """
        self.hold()

        launch_context = Gdk.Display.get_app_launch_context(Gdk.Display.get_default())
        launch_context.set_timestamp(timestamp)
        search_text = " ".join(terms)
        app = Gio.AppInfo.create_from_commandline(
            f'iotas --search "{search_text}"',
            const.APP_ID,
            Gio.AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION,
        )
        if not app:
            logging.error("Failed to create AppInfo")
        elif not app.launch(None, launch_context):
            logging.error("Failed to launch")

        self.release()

    def __search(self, terms: list[str], previous_results: list[int] = []) -> list[str]:
        search_text = " ".join(terms)
        return [str(x) for x in self.__db.search_notes(search_text, previous_results, True)]


def main():
    service = SearchIotasService()
    service.run()


if __name__ == "__main__":
    main()
