#!/usr/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('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/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:
            if 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 != '':
                error_id = ERROR_PREFIX + 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/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.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.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())
