#!/usr/bin/python3
from gi.repository import Gio, GLib

import logging
from typing import Dict, List

from iotas.database import Database
from iotas.note_database import NoteDatabase


class Server:
    def __init__(self, conn: Gio.DBusConnection, 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(self.DBUS_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("__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 = {}
        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.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()
        try:
            argv = ["iotas", "--open-note", search_id]
            (pid, stdin, stdout, stderr) = GLib.spawn_async(
                argv,
                flags=GLib.SpawnFlags.SEARCH_PATH,
                standard_input=False,
                standard_output=False,
                standard_error=False,
            )
            GLib.spawn_close_pid(pid)
        except Exception as e:
            logging.warning("ActivateResult", e)
        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("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:
            ids = [int(x) for x in ids]
            notes = self.__db.get_notes_by_ids(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("GetResultMetas", e)
            return []
        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()
        if len(previous_results) < self.MAX_SUBSEARCH_LIST_LENGTH:
            ids = [int(x) for x in previous_results]
        else:
            ids = []

        results = []
        try:
            results = self.__search(new_terms, ids)
        except Exception as e:
            logging.warning("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()
        search_text = " ".join(terms)
        try:
            argv = ["iotas", "--search", search_text]
            (pid, stdin, stdout, stderr) = GLib.spawn_async(
                argv,
                flags=GLib.SpawnFlags.SEARCH_PATH,
                standard_input=False,
                standard_output=False,
                standard_error=False,
            )
            GLib.spawn_close_pid(pid)
        except Exception as e:
            logging.warning("LaunchSearch", e)
        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()
