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

import gettext
import locale
import sys

import gi

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

from dialect.providers import TRANSLATORS
from dialect.providers.base import ProviderErrorCode
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 TranslateService:
    def __init__(self):
        self.loaded = False
        self.load_failed = False

        # Live translation enabled
        self.live_enabled = self.is_live_enabled()

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

        # Translator
        self._load_translator()
        Settings.get().connect("changed", self._on_settings_changed)
        Settings.get().connect("translator-changed", self._on_translator_changed)

    def GetInitialResultSet(self, terms, callback):
        """
        Join separate terms in one ID line, start translation and send this line back
        on start of input
        """

        def on_done(translation):
            self.translations[text] = translation.text
            callback([text, CLIPBOARD_PREFIX + text])

        def on_fail(error):
            match error.code:
                case ProviderErrorCode.NETWORK:
                    self.translations[error_id] = _("Translation failed, check for network issues")
                case ProviderErrorCode.API_KEY_INVALID:
                    self.translations[error_id] = _("The provided API key is invalid")
                case ProviderErrorCode.API_KEY_REQUIRED:
                    self.translations[error_id] = _("API key is required to use the service")
                case _:
                    self.translations[error_id] = _("Translation failed")
            callback([error_id])

        text = " ".join(terms)

        if self.live_enabled:
            error_id = ERROR_PREFIX + text

            if self.load_failed:
                self.translations[error_id] = _("Failed loading the translation service")
                callback([error_id])
            elif not self.loaded:
                return self.GetInitialResultSet(terms)

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

        else:
            provider = Settings.get().active_translator

            callback(
                [
                    _("Translate “{text}” with {provider_name}").format(
                        text=text, provider_name=TRANSLATORS[provider].prettyname
                    )
                ]
            )

    def GetSubsearchResultSet(self, _previous_results, new_terms, callback):
        self.GetInitialResultSet(new_terms, callback)

    def GetResultMetas(self, ids, callback):
        """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]

            callback(
                [
                    {
                        "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]
            lang = self.translator.get_lang_name(self.dest_language)
            provider = Settings.get().active_translator
            description = f"{lang} — {TRANSLATORS[provider].prettyname}" if self.live_enabled else ""

            self.translations.clear()

            callback(
                [
                    {
                        "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
            callback(
                [
                    dict(
                        id=GLib.Variant("s", id),
                        name=GLib.Variant("s", id),
                    )
                    for id in ids
                ]
            )

    def ActivateResult(self, result_id, terms, timestamp, callback):
        if not result_id.startswith(CLIPBOARD_PREFIX):
            self.LaunchSearch(terms, timestamp)

        callback((None,))

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

    def is_live_enabled(self):
        return Settings.get().live_translation and Settings.get().sp_translation

    def _load_translator(self):
        def on_done():
            self.loaded = True
            self.load_failed = False
            self.dest_language = self.translator.recent_dest_langs[0]

            self.translator.settings.connect("changed", self._on_translator_settings_changed)

        def on_fail(_error):
            self.loaded = False
            self.load_failed = True
            self.dest_language = None

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

        # Init translator
        self.translator.init_trans(on_done, on_fail)

    def _on_settings_changed(self, _settings, key):
        if key.startswith("translator-"):
            self._load_translator()
        if key == "live-translation" or key == "sp-translation":
            self.live_enabled = self.is_live_enabled()

    def _on_translator_changed(self, *args):
        self._load_translator()

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


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.service_object = TranslateService()
        self.search_interface = Gio.DBusNodeInfo.new_for_xml(dbus_interface_description).interfaces[0]

    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, connection, sender, object_path, interface_name, method_name, parameters, invocation):
        def return_value(results):
            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,
                    )
                )
                + ")"
            )
            wrapped_results = GLib.Variant(results_type, results)

            invocation.return_value(wrapped_results)

            self.release()

        self.hold()

        method = getattr(self.service_object, method_name)
        arguments = list(parameters.unpack())
        arguments.append(return_value)

        method(*arguments)


if __name__ == "__main__":
    app = TranslateServiceApplication()
    sys.exit(app.run())
