# -*- coding: utf-8 -*-

import xmlrpc.client
import threading
import zlib
from os import sep
import gettext

import gi
gi.require_version('Peas', '1.0')
gi.require_version('Gtk', '3.0')
gi.require_version('Totem', '1.0')
from gi.repository import GLib, GObject # pylint: disable=wrong-import-position
from gi.repository import Peas, Gtk, Gdk # pylint: disable=wrong-import-position,
from gi.repository import Gio, Pango, Totem # pylint: disable=wrong-import-position,no-name-in-module

from hash import hash_file # pylint: disable=wrong-import-position

gettext.textdomain ("totem")

D_ = gettext.dgettext
_ = gettext.gettext

USER_AGENT = 'Totem'
OK200 = '200 OK'
TOTEM_REMOTE_COMMAND_REPLACE = 14

SUBTITLES_EXT = [
    "asc",
    "txt",
    "sub",
    "vtt",
    "srt",
    "smi",
    "ssa",
    "ass",
]

# Map of the language codes used by opensubtitles.org's API to their
# human-readable name
LANGUAGES_STR = [ (D_('iso_639_3', 'Albanian'), 'sq'),
         (D_('iso_639_3', 'Arabic'), 'ar'),
         (D_('iso_639_3', 'Armenian'), 'hy'),
         (D_('iso_639_3', 'Neo-Aramaic, Assyrian'), 'ay'),
         (D_('iso_639_3', 'Basque'), 'eu'),
         (D_('iso_639_3', 'Bosnian'), 'bs'),
         (_('Brazilian Portuguese'), 'pb'),
         (D_('iso_639_3', 'Bulgarian'), 'bg'),
         (D_('iso_639_3', 'Catalan'), 'ca'),
         (D_('iso_639_3', 'Chinese'), 'zh'),
         (D_('iso_639_3', 'Croatian'), 'hr'),
         (D_('iso_639_3', 'Czech'), 'cs'),
         (D_('iso_639_3', 'Danish'), 'da'),
         (D_('iso_639_3', 'Dutch'), 'nl'),
         (D_('iso_639_3', 'English'), 'en'),
         (D_('iso_639_3', 'Esperanto'), 'eo'),
         (D_('iso_639_3', 'Estonian'), 'et'),
         (D_('iso_639_3', 'Finnish'), 'fi'),
         (D_('iso_639_3', 'French'), 'fr'),
         (D_('iso_639_3', 'Galician'), 'gl'),
         (D_('iso_639_3', 'Georgian'), 'ka'),
         (D_('iso_639_3', 'German'), 'de'),
         (D_('iso_639_3', 'Greek, Modern (1453-)'), 'el'),
         (D_('iso_639_3', 'Hebrew'), 'he'),
         (D_('iso_639_3', 'Hindi'), 'hi'),
         (D_('iso_639_3', 'Hungarian'), 'hu'),
         (D_('iso_639_3', 'Icelandic'), 'is'),
         (D_('iso_639_3', 'Indonesian'), 'id'),
         (D_('iso_639_3', 'Italian'), 'it'),
         (D_('iso_639_3', 'Japanese'), 'ja'),
         (D_('iso_639_3', 'Kazakh'), 'kk'),
         (D_('iso_639_3', 'Korean'), 'ko'),
         (D_('iso_639_3', 'Latvian'), 'lv'),
         (D_('iso_639_3', 'Lithuanian'), 'lt'),
         (D_('iso_639_3', 'Luxembourgish'), 'lb'),
         (D_('iso_639_3', 'Macedonian'), 'mk'),
         (D_('iso_639_3', 'Malay (macrolanguage)'), 'ms'),
         (D_('iso_639_3', 'Norwegian'), 'no'),
         (D_('iso_639_3', 'Occitan (post 1500)'), 'oc'),
         (D_('iso_639_3', 'Persian'), 'fa'),
         (D_('iso_639_3', 'Polish'), 'pl'),
         (D_('iso_639_3', 'Portuguese'), 'pt'),
         (D_('iso_639_3', 'Romanian'), 'ro'),
         (D_('iso_639_3', 'Russian'), 'ru'),
         (D_('iso_639_3', 'Serbian'), 'sr'),
         (D_('iso_639_3', 'Slovak'), 'sk'),
         (D_('iso_639_3', 'Slovenian'), 'sl'),
         (D_('iso_639_3', 'Spanish'), 'es'),
         (D_('iso_639_3', 'Swedish'), 'sv'),
         (D_('iso_639_3', 'Thai'), 'th'),
         (D_('iso_639_3', 'Turkish'), 'tr'),
         (D_('iso_639_3', 'Ukrainian'), 'uk'),
         (D_('iso_639_3', 'Vietnamese'), 'vi'),]

# Map of ISO 639-1 language codes to the codes used by opensubtitles.org's API
LANGUAGES = {'sq':'alb',
         'ar':'ara',
         'hy':'arm',
         'ay':'ass',
         'bs':'bos',
         'pb':'pob',
         'bg':'bul',
         'ca':'cat',
         'zh':'chi',
         'hr':'hrv',
         'cs':'cze',
         'da':'dan',
         'nl':'dut',
         'en':'eng',
         'eo':'epo',
         'eu':'eus',
         'et':'est',
         'fi':'fin',
         'fr':'fre',
         'gl':'glg',
         'ka':'geo',
         'de':'ger',
         'el':'ell',
         'he':'heb',
         'hi':'hin',
         'hu':'hun',
         'is':'ice',
         'id':'ind',
         'it':'ita',
         'ja':'jpn',
         'kk':'kaz',
         'ko':'kor',
         'lv':'lav',
         'lt':'lit',
         'lb':'ltz',
         'mk':'mac',
         'ms':'may',
         'no':'nor',
         'oc':'oci',
         'fa':'per',
         'pl':'pol',
         'pt':'por',
         'ro':'rum',
         'ru':'rus',
         'sr':'scc',
         'sk':'slo',
         'sl':'slv',
         'es':'spa',
         'sv':'swe',
         'th':'tha',
         'tr':'tur',
         'uk':'ukr',
         'vi':'vie',}

def _cache_subtitles_dir ():
    bpath = GLib.get_user_cache_dir() + sep
    bpath += 'totem' + sep + 'subtitles' + sep
    return bpath

class SearchThread (threading.Thread):
    """
    This is the thread started when the dialog is searching for subtitles
    """
    def __init__ (self, model, movie_hash, movie_size):
        self._model = model
        self._movie_hash = movie_hash
        self._movie_size = movie_size
        self._done = False
        self._results = []
        self._lock = threading.Lock ()
        self._message = ''
        threading.Thread.__init__ (self)

    def run (self):
        self._lock.acquire (True)
        (self._results,
         self._message) = self._model.search_subtitles (self._movie_hash,
                                                        self._movie_size)
        self._done = True
        self._lock.release ()

    def get_results (self):
        results = []

        self._lock.acquire (True)
        if self._done:
            results = self._results
        self._lock.release ()

        return results

    def get_message (self):
        message = _(u'Searching for subtitles…')

        self._lock.acquire (True)
        if self._done:
            message = self._message
        self._lock.release ()

        return message

    @property
    def done (self):
        """ Thread-safe property to know whether the query is done or not """
        self._lock.acquire (True)
        res = self._done
        self._lock.release ()
        return res

class DownloadThread (threading.Thread):
    """
    This is the thread started when the dialog is downloading the subtitles.
    """
    def __init__ (self, model, subtitle_id):
        self._model = model
        self._subtitle_id = subtitle_id
        self._done = False
        self._lock = threading.Lock ()
        self._subtitles = ''
        self._message = ''
        threading.Thread.__init__ (self)

    def run (self):
        self._lock.acquire (True)
        (self._subtitles,
         self._message) = self._model.download_subtitles (self._subtitle_id)
        self._done = True
        self._lock.release ()

    def get_subtitles (self):
        subtitles = ''

        self._lock.acquire (True)
        if self._done:
            subtitles = self._subtitles
        self._lock.release ()

        return subtitles

    def get_message (self):
        message = _(u'Downloading the subtitles…')

        self._lock.acquire (True)
        if self._done:
            message = self._message
        self._lock.release ()

        return message

    @property
    def done (self):
        """ Thread-safe property to know whether the query is done or not """
        self._lock.acquire (True)
        res = self._done
        self._lock.release ()
        return res

# OpenSubtitles.org API abstraction

class OpenSubtitlesModel:
    """
    This contains the logic of the opensubtitles service.
    """
    def __init__ (self, server):
        self._server = server
        self._token = None

        try:
            import locale # pylint: disable=import-outside-toplevel
            (language_code, _) = locale.getlocale ()
            self.lang = LANGUAGES[language_code.split ('_')[0]]
        except (ImportError, IndexError, AttributeError, KeyError):
            self.lang = 'eng'

        self._lock = threading.Lock ()

    def _log_in (self, username='', password=''):
        """
        Non-locked version of log_in() for internal use only.

        @rtype : (bool, string)
        """

        result = None

        if self._token:
            # We have already logged-in before, check the connection
            try:
                result = self._server.NoOperation (self._token)
            except (xmlrpc.client.Fault, xmlrpc.client.ProtocolError):
                pass
            if result and result['status'] != OK200:
                return (True, '')

        try:
            result = self._server.LogIn (username, password, self.lang,
                                         USER_AGENT)
        except (xmlrpc.client.Fault, xmlrpc.client.ProtocolError):
            pass

        if result and result.get ('status') == OK200:
            self._token = result.get ('token')
            if self._token:
                return (True, '')

        return (False, _(u'Could not contact the OpenSubtitles website'))

    def log_in (self, username='', password=''):
        """
        Logs into the opensubtitles web service and gets a valid token for
        the comming comunications. If we are already logged it only checks
        the if the token is still valid. It returns a tuple of success boolean
        and error message (if appropriate).

        @rtype : (bool, string)
        """

        self._lock.acquire (True)
        result = self._log_in (username, password)
        self._lock.release ()

        return result

    def search_subtitles (self, movie_hash, movie_size):
        self._lock.acquire (True)

        message = ''

        (log_in_success, log_in_message) = self._log_in ()

        if log_in_success:
            searchdata = {'sublanguageid': self.lang,
                          'moviehash'    : movie_hash,
                          'moviebytesize': str (movie_size)}
            try:
                result = self._server.SearchSubtitles (self._token,
                                                       [searchdata])
            except xmlrpc.client.ProtocolError:
                message = _(u'Could not contact the OpenSubtitles website.')

            if result.get ('data'):
                self._lock.release ()
                return (result['data'], message)

            message = _(u'No results found.')
        else:
            message = log_in_message

        self._lock.release ()

        return (None, message)

    def download_subtitles (self, subtitle_id):
        self._lock.acquire (True)

        message = ''
        error_message = _(u'Could not contact the OpenSubtitles website.')

        (log_in_success, log_in_message) = self._log_in ()

        if log_in_success:
            result = None
            try:
                result = self._server.DownloadSubtitles (self._token,
                                                         [subtitle_id])
            except xmlrpc.client.ProtocolError:
                message = error_message

            if result and result.get ('status') == OK200:
                try:
                    subtitle64 = result['data'][0]['data']
                except LookupError:
                    self._lock.release ()
                    return (None, error_message)

                subtitle_unzipped = zlib.decompress(GLib.base64_decode (subtitle64), 47)

                self._lock.release ()

                return (subtitle_unzipped, message)
        else:
            message = log_in_message

        self._lock.release ()

        return (None, message)

class OpenSubtitles (GObject.Object, # pylint: disable=R0902
                     Peas.Activatable):
    __gtype_name__ = 'OpenSubtitles'

    object = GObject.Property (type = GObject.Object)

    def __init__ (self):
        GObject.Object.__init__ (self)

        self._dialog = None
        self._totem = None
        schema = 'org.gnome.totem.plugins.opensubtitles'
        self._settings = Gio.Settings.new (schema)

        self._action = None

        self._find_button = None
        self._apply_button = None
        self._close_button = None

        self._list_store = None
        self._model = None
        self._tree_view = None

        self._filename = None
        self._progress = None

    # totem.Plugin methods

    def do_activate (self):
        """
        Called when the plugin is activated.
        Here the sidebar page is initialized (set up the treeview, connect
        the callbacks, ...) and added to totem.
        """
        self._totem = self.object

        # Name of the movie file which the most-recently-downloaded subtitles
        # are related to.
        self._filename = None

        self._append_menu ()

        self._totem.connect ('file-opened', self.__on_totem__file_opened) # pylint: disable=no-member
        self._totem.connect ('file-closed', self.__on_totem__file_closed) # pylint: disable=no-member

        # Obtain the ServerProxy and init the model
        server = xmlrpc.client.Server ('http://api.opensubtitles.org/xml-rpc')
        self._model = OpenSubtitlesModel (server)

        mrl = self._totem.get_current_mrl ()
        if mrl:
            self.__on_totem__file_opened(self._totem, self._totem.get_current_mrl ())

    def do_deactivate (self):
        if self._dialog:
            self._dialog.destroy ()
        self._dialog = None

        self._delete_menu ()

    # UI related code

    def _build_dialog (self):
        builder = Totem.plugin_load_interface ("opensubtitles",
                                               "opensubtitles.ui", True,
                                               self._totem.get_main_window (), # pylint: disable=no-member
                                               None)

        # Obtain all the widgets we need to initialize
        combobox = builder.get_object ('language_combobox')
        languages = builder.get_object ('language_model')
        self._progress = builder.get_object ('progress_bar')
        self._tree_view = builder.get_object ('subtitle_treeview')
        self._list_store = builder.get_object ('subtitle_model')
        self._dialog = builder.get_object ('subtitles_dialog')
        self._find_button = builder.get_object ('find_button')
        self._apply_button = builder.get_object ('apply_button')
        self._close_button = builder.get_object ('close_button')

        # Set up and populate the languages combobox
        renderer = Gtk.CellRendererText ()
        sorted_languages = Gtk.TreeModelSort (model = languages)
        sorted_languages.set_sort_column_id (0, Gtk.SortType.ASCENDING)
        combobox.set_model (sorted_languages)
        combobox.pack_start (renderer, True)
        combobox.add_attribute (renderer, 'text', 0)

        lang = self._settings.get_string ('language')
        if lang is not None:
            self._model.lang = lang

        for lang in LANGUAGES_STR:
            itera = languages.append (lang)
            if LANGUAGES[lang[1]] == self._model.lang:
                (success,
                 parentit) = sorted_languages.convert_child_iter_to_iter (itera)
                if success:
                    combobox.set_active_iter (parentit)

        # Set up the results treeview
        renderer = Gtk.CellRendererText ()
        self._tree_view.set_model (self._list_store)
        renderer.set_property ('ellipsize', Pango.EllipsizeMode.END)
        column = Gtk.TreeViewColumn (_(u"Subtitles"), renderer, text=0)
        column.set_resizable (True)
        column.set_expand (True)
        self._tree_view.append_column (column)
        # translators comment:
        # This is the file-type of the subtitle file detected
        column = Gtk.TreeViewColumn (_(u"Format"), renderer, text=1)
        column.set_resizable (False)
        self._tree_view.append_column (column)
        # translators comment:
        # This is a rating of the quality of the subtitle
        column = Gtk.TreeViewColumn (_(u"Rating"), renderer, text=2)
        column.set_resizable (False)
        self._tree_view.append_column (column)

        self._apply_button.set_sensitive (False)

        self._apply_button.connect ('clicked', self.__on_apply_clicked)
        self._find_button.connect ('clicked', self.__on_find_clicked)
        self._close_button.connect ('clicked', self.__on_close_clicked)

        # Set up signals

        combobox.connect ('changed', self.__on_combobox__changed)
        self._dialog.set_transient_for (self._totem.get_main_window ()) # pylint: disable=no-member
        self._dialog.set_position (Gtk.WindowPosition.CENTER_ON_PARENT)

        # Connect the callbacks
        self._dialog.connect ('key-press-event',
                             self.__on_window__key_press_event)
        self._tree_view.get_selection ().connect ('changed',
                                                self.__on_treeview__row_change)
        self._tree_view.connect ('row-activated',
                               self.__on_treeview__row_activate)

    def _show_dialog (self, *_):
        if not self._dialog:
            self._build_dialog ()

        self._dialog.show_all ()

        self._progress.set_fraction (0.0)

    def _append_menu (self):
        self._action = Gio.SimpleAction.new ("opensubtitles", None)
        self._action.connect ('activate', self._show_dialog)
        self._totem.add_action (self._action) # pylint: disable=no-member
        self._totem.set_accels_for_action ("app.opensubtitles",  # pylint: disable=no-member
                                           ["<Primary><Shift>s"])

        menu = self._totem.get_menu_section ("subtitle-download-placeholder") # pylint: disable=no-member
        menu.append (_(u'_Download Movie Subtitles…'), "app.opensubtitles")

        self._action.set_enabled (self._totem.is_playing () and # pylint: disable=no-member
                  self._check_allowed_scheme () and
                                  not self._check_is_audio ())

    def _check_allowed_scheme (self):
        current_file = Gio.file_new_for_uri (self._totem.get_current_mrl ()) # pylint: disable=no-member
        scheme = current_file.get_uri_scheme ()

        if scheme in ('dvd', 'http', 'dvb', 'vcd'):
            return False

        return True

    def _check_is_audio (self):
        # FIXME need to use something else here
        # I think we must use video widget metadata but I don't found a way
        # to get this info from python
        filename = self._totem.get_current_mrl () # pylint: disable=no-member
        if Gio.content_type_guess (filename, '')[0].split ('/')[0] == 'audio':
            return True
        return False

    def _delete_menu (self):
        self._totem.empty_menu_section ("subtitle-download-placeholder") # pylint: disable=no-member

    def _get_results (self, movie_hash, movie_size):
        self._list_store.clear ()
        self._apply_button.set_sensitive (False)
        self._find_button.set_sensitive (False)

        cursor = Gdk.Cursor.new (Gdk.CursorType.WATCH)
        self._dialog.get_window ().set_cursor (cursor)

        thread = SearchThread (self._model, movie_hash, movie_size)
        thread.start ()
        GLib.idle_add (self._populate_treeview, thread)

        self._progress.set_text (_(u'Searching subtitles…'))
        GLib.timeout_add (350, self._progress_bar_increment, thread)

    def _populate_treeview (self, search_thread):
        if not search_thread.done:
            return True

        results = search_thread.get_results ()
        if results:
            for sub_data in results:
                if not SUBTITLES_EXT.count (sub_data['SubFormat']):
                    continue
                self._list_store.append ([sub_data['SubFileName'],
                                        sub_data['SubFormat'],
                                        sub_data['SubRating'],
                                        sub_data['IDSubtitleFile'],])

        self._dialog.get_window ().set_cursor (None)

        return False

    def _save_selected_subtitle (self):
        cursor = Gdk.Cursor.new (Gdk.CursorType.WATCH)
        self._dialog.get_window ().set_cursor (cursor)

        model, rows = self._tree_view.get_selection ().get_selected_rows ()
        if rows:
            subtitle_iter = model.get_iter (rows[0])
            subtitle_id = model.get_value (subtitle_iter, 3)
            subtitle_format = model.get_value (subtitle_iter, 1)

            bpath = _cache_subtitles_dir()

            directory = Gio.file_new_for_path (bpath)
            try:
                directory.make_directory_with_parents (None)
            except: # pylint: disable=bare-except
                pass

            thread = DownloadThread (self._model, subtitle_id)
            thread.start ()
            GLib.idle_add (self._save_subtitles, thread, subtitle_format)

            self._progress.set_text (_(u'Downloading the subtitles…'))
            GLib.timeout_add (350, self._progress_bar_increment, thread)
        else:
            #warn user!
            pass

    def _movie_dir (self):
        directory = Gio.file_new_for_uri (self._filename)
        parent = directory.get_parent()
        return parent.get_path ()

    def _save_subtitles (self, download_thread, extension):
        if not download_thread.done:
            return True

        subtitles = download_thread.get_subtitles ()
        suburi = None
        if subtitles:
            subtitle_file = Gio.file_new_for_uri (self._filename)
            movie_name = subtitle_file.get_basename ().rpartition ('.')[0]

            # Delete all previous cached subtitle for this file
            for ext in SUBTITLES_EXT:
                # In the cache dir
                old_subtitle_file = Gio.file_new_for_path (_cache_subtitles_dir() +
                        sep + movie_name + '.' + ext)
                try:
                    old_subtitle_file.delete (None)
                except: # pylint: disable=bare-except
                    pass

                # In the movie dir
                old_subtitle_file = Gio.file_new_for_path (self._movie_dir() +
                        sep + movie_name + '.' + ext)
                try:
                    old_subtitle_file.delete (None)
                except: # pylint: disable=bare-except
                    pass

            flags = Gio.FileCreateFlags.REPLACE_DESTINATION
            try:
                subtitle_file = Gio.file_new_for_path (self._movie_dir() +
                        sep + movie_name + '.' + extension)
                suburi = subtitle_file.get_uri ()

                sub_file = subtitle_file.replace ('', False, flags, None)
                sub_file.write (subtitles, None)
                sub_file.close (None)
            except: # pylint: disable=bare-except
                subtitle_file = Gio.file_new_for_path (_cache_subtitles_dir() +
                        sep + movie_name + '.' + extension)
                suburi = subtitle_file.get_uri ()

                sub_file = subtitle_file.replace ('', False, flags, None)
                sub_file.write (subtitles, None)
                sub_file.close (None)

        self._dialog.get_window ().set_cursor (None)
        self._close_dialog ()

        if suburi:
            self._totem.set_current_subtitle (suburi) # pylint: disable=no-member

        return False

    def _progress_bar_increment (self, thread):
        if not thread.done:
            self._progress.pulse ()
            return True

        message = thread.get_message ()
        if message:
            self._progress.set_text (message)
        else:
            self._progress.set_text (' ')

        self._progress.set_fraction (0.0)
        self._find_button.set_sensitive (True)
        self._apply_button.set_sensitive (False)
        self._tree_view.set_sensitive (True)
        return False

    def _download_and_apply (self):
        self._apply_button.set_sensitive (False)
        self._find_button.set_sensitive (False)
        self._action.set_enabled (False)
        self._tree_view.set_sensitive (False)
        self._save_selected_subtitle ()

    def _close_dialog (self):
        # We hide the dialogue instead of closing it so that we still have the
        # last set of search results up if we re-open the dialogue without
        # changing the movie
        self._dialog.hide ()

    # Callbacks

    def __on_window__key_press_event (self, _, event):
        if event.keyval == Gdk.KEY_Escape:
            self._close_dialog ()
            return True
        return False

    def __on_treeview__row_change (self, selection):
        if selection.count_selected_rows () > 0:
            self._apply_button.set_sensitive (True)
        else:
            self._apply_button.set_sensitive (False)

    def __on_treeview__row_activate (self,
                                     _tree_path, # pylint: disable=W0613
                                     _column, # pylint: disable=W0613
                                     _data): # pylint: disable=W0613
        self._download_and_apply ()

    def __on_totem__file_opened (self, _, new_mrl):
        # Check if allows subtitles
        if self._check_allowed_scheme () and not self._check_is_audio ():
            self._action.set_enabled (True)
            if self._dialog:
                self._find_button.set_sensitive (True)
                # Check we're not re-opening the same file; if we are, don't
                # clear anything. This happens when we re-load the file with a
                # new set of subtitles, for example
                if self._filename != new_mrl:
                    self._filename = new_mrl
                    self._list_store.clear ()
                    self._apply_button.set_sensitive (False)
        else:
            self._action.set_enabled (False)
            if self._dialog and self._dialog.is_active ():
                self._filename = None
                self._list_store.clear ()
                self._apply_button.set_sensitive (False)
                self._find_button.set_sensitive (False)

    def __on_totem__file_closed (self, _):
        self._action.set_enabled (False)
        if self._dialog:
            self._apply_button.set_sensitive (False)
            self._find_button.set_sensitive (False)

    def __on_combobox__changed (self, combobox):
        combo_iter = combobox.get_active_iter ()
        combo_model = combobox.get_model ()
        self._model.lang = LANGUAGES[combo_model.get_value (combo_iter, 1)]
        self._settings.set_string ('language', self._model.lang)

    def __on_close_clicked (self, _):
        self._close_dialog ()

    def __on_apply_clicked (self, _):
        self._download_and_apply ()

    def __on_find_clicked (self, _):
        self._apply_button.set_sensitive (False)
        self._find_button.set_sensitive (False)
        self._filename = self._totem.get_current_mrl () # pylint: disable=no-member
        (movie_hash, movie_size) = hash_file (self._filename)

        self._get_results (movie_hash, movie_size)
