#!/usr/local/bin/python3
# Copyright 2020 gi-lom
# Copyright 2020 Nikita Kravets
# Copyright 2020 Rafael Mardojai CM
# Copyright 2021 Mufeed Ali
# Copyright 2023 Markus Göllnitz
# SPDX-License-Identifier: GPL-3.0-or-later

import gettext
import locale
import sys
import inspect
import logging
from typing import Any, Callable, Coroutine

import gi

gi.require_version("Secret", "1")
gi.require_version("Soup", "3.0")
from gi.repository import Gio, GLib

from dialect.asyncio import create_background_task, glib_event_loop_policy
from dialect.providers import (
    TRANSLATORS,
    TranslationRequest,
    ProviderError,
    RequestError,
    APIKeyInvalid,
    APIKeyRequired,
)
from dialect.settings import Settings

CLIPBOARD_PREFIX = "copy-to-clipboard"
ERROR_PREFIX = "translation-error"

localedir = "/usr/local/share/locale"
langs_trans = gettext.translation("dialect-cldr-langs", localedir, fallback=True)
ui_trans = gettext.translation("dialect", localedir, fallback=True)
ui_trans.add_fallback(langs_trans)
ui_trans.install(names=["gettext"])

locale.bindtextdomain("dialect", localedir)
locale.textdomain("dialect")

dbus_interface_description = """
<!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>
"""


class TranslateServiceApplication(Gio.Application):
    def __init__(self):
        Gio.Application.__init__(
            self,
            application_id="app.drey.Dialect.SearchProvider",
            flags=Gio.ApplicationFlags.IS_SERVICE,
            inactivity_timeout=10000,
        )
        self.search_interface = Gio.DBusNodeInfo.new_for_xml(dbus_interface_description).interfaces[0]

        self.loaded = False
        self.translations = {}  # Translations store
        self.src_language = "auto"
        self.dest_language = None

        # Translator
        Settings.get().connect("provider-changed::translator", self._on_translator_changed)

    def do_dbus_register(self, connection, object_path):
        try:
            connection.register_object(
                object_path=object_path,
                interface_info=self.search_interface,
                method_call_closure=self.on_dbus_method_call,
            )
        except:
            self.quit()
            return False
        finally:
            return True

    def on_dbus_method_call(
        self,
        _conn: Gio.DBusConnection,
        _sender: str,
        _object_path: str,
        _interface_name: str,
        method_name: str,
        parameters: GLib.Variant,
        invocation: Gio.DBusMethodInvocation,
    ):

        def wrap_results(results: Any) -> GLib.Variant:
            results = (results,)
            if results == (None,):
                results = ()
            results_type = (
                "("
                + "".join(
                    map(
                        lambda argument_info: argument_info.signature,
                        self.search_interface.lookup_method(method_name).out_args,  # type: ignore
                    )
                )
                + ")"
            )

            return GLib.Variant(results_type, results)

        async def return_async_value(method: Callable[..., Coroutine], *args):
            results = wrap_results(await method(*args))
            self.release()
            invocation.return_value(results)

        method = getattr(self, method_name)
        args = list(parameters.unpack())

        if inspect.iscoroutinefunction(method):  # Async methods
            create_background_task(return_async_value(method, *args))
            self.hold()
        else:  # Sync methods
            results = wrap_results(method(*args))
            invocation.return_value(results)

    @property
    def live_enabled(self) -> bool:
        return Settings.get().live_translation and Settings.get().sp_translation

    async def GetInitialResultSet(self, terms: list[str]) -> list[str]:
        """
        Join separate terms in one ID line, start translation and send this line back
        on start of input
        """
        text = " ".join(terms)

        if self.live_enabled:
            error_id = ERROR_PREFIX + text

            # Load the translator if needed
            # TODO: Verify API key when needed
            if not self.loaded:
                try:
                    await self._load_translator()
                except Exception:
                    self.translations[error_id] = _("Failed loading the translation service")
                    return [error_id]

            # If the two languages are the same, nothing is done
            if self.dest_language and self.src_language != self.dest_language and text != "":
                src, dest = self.translator.denormalize_lang(self.src_language, self.dest_language)
                request = TranslationRequest(text, src, dest)

                try:
                    translation = await self.translator.translate(request)
                    self.translations[text] = translation.text
                    return [text, CLIPBOARD_PREFIX + text]
                except (RequestError, ProviderError) as exc:
                    logging.error(exc)

                    if isinstance(exc, RequestError):
                        self.translations[error_id] = _("Translation failed, check for network issues")
                    elif isinstance(exc, APIKeyInvalid):
                        self.translations[error_id] = _("The provided API key is invalid")
                    elif isinstance(exc, APIKeyRequired):
                        self.translations[error_id] = _("API key is required to use the service")
                    else:
                        self.translations[error_id] = _("Translation failed")

                    return [error_id]
            else:
                return []
        else:
            return [
                _("Translate “{text}” with {provider_name}").format(
                    text=text, provider_name=TRANSLATORS[Settings.get().active_translator].prettyname
                )
            ]

    async def GetSubsearchResultSet(self, _previous_results: list[str], new_terms: list[str]) -> list[str]:
        return await self.GetInitialResultSet(new_terms)

    def GetResultMetas(self, ids: list[str]) -> list[dict[str, GLib.Variant]]:
        """Send translated text"""

        translate_id = ids[0]

        if len(ids) == 1:
            text = translate_id
            if translate_id in self.translations:
                text = self.translations[translate_id]

            return [
                {
                    "id": GLib.Variant("s", translate_id),
                    "name": GLib.Variant("s", text),
                }
            ]

        elif len(ids) == 2 and translate_id in self.translations and ids[1] == CLIPBOARD_PREFIX + ids[0]:
            text = self.translations[translate_id]
            description = ""

            if self.dest_language:
                lang = self.translator.get_lang_name(self.dest_language)
                provider = Settings.get().active_translator
                description = f"{lang or self.dest_language} — {TRANSLATORS[provider].prettyname}"

            self.translations.clear()

            return [
                {
                    "id": GLib.Variant("s", translate_id),
                    "name": GLib.Variant("s", text),
                    "description": GLib.Variant("s", description),
                },
                {
                    "id": GLib.Variant("s", ids[1]),
                    "name": GLib.Variant("s", _("Copy")),
                    "description": GLib.Variant("s", _("Copy translation to clipboard")),
                    "clipboardText": GLib.Variant("s", text),
                },
            ]

        else:
            # Probably never needed, just in case
            return [
                dict(
                    id=GLib.Variant("s", id),
                    name=GLib.Variant("s", id),
                )
                for id in ids
            ]

    def ActivateResult(self, result_id: str, terms: list[str], timestamp: int):
        if not result_id.startswith(CLIPBOARD_PREFIX):
            self.LaunchSearch(terms, timestamp)

    def LaunchSearch(self, terms: list[str], _timestamp: int):
        text = " ".join(terms)
        GLib.spawn_async_with_pipes(None, ["/usr/local/bin/dialect", "--text", text], None, GLib.SpawnFlags.SEARCH_PATH, None)

    async def _load_translator(self):
        if self.loaded:
            return

        self.translator = TRANSLATORS[Settings.get().active_translator]()

        # Init translator
        try:
            await self.translator.init_trans()

            self.loaded = True
            self.dest_language = self.translator.recent_dest_langs[0]
            self.translator.settings.connect("changed", self._on_translator_settings_changed)

        except Exception:
            self.dest_language = None
            raise

    def _on_translator_changed(self, *args):
        self.loaded = False

    def _on_translator_settings_changed(self, _settings, key: str):
        if key == "src-langs" or key == "dest-langs":
            self.dest_language = self.translator.recent_dest_langs[0]
        else:
            self.loaded = False


def main():
    app = TranslateServiceApplication()
    exit_code = 0
    with glib_event_loop_policy():
        exit_code = app.run(None)
    return exit_code


if __name__ == "__main__":
    sys.exit(main())
