# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2006-2009 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.
#
# Authors: Benjamin Kampmann <benjamin@fluendo.com>
#          Olivier Tilloy <olivier@fluendo.com>

"""
Checks for updates on the internet
"""

from urllib import quote

from elisa.core import common
from elisa.core.utils import defer, misc
from elisa.core.utils.internet import get_page
from elisa.core.log import Loggable
from elisa.core.plugin import Plugin
from elisa.core.components.message import Message

from twisted.internet import reactor, error, task

from distutils.version import LooseVersion


"""
the url used for lookup
"""
HOST = "www.moovida.com"
UPDATES_URL = "http://" + HOST + "/updates/updates/?" \
              "install_date=%(date)s&version=%(version)s&os=%(os)s" \
              "&user_id=%(user_id)s&aen=%(aen)s&entity_id=%(entity_id)s" \
              "&referrer=%(referrer)s&traffic_unit=%(traffic_unit)s"


class AlreadyRunning(Exception):
    pass


class AvailablePluginUpdatesMessage(Message):

    """
    Message sent when plugins updates and/or new recommended plugins are
    available for download from the plugin repository.

    @ivar available_updates: the list of plugins available for update
    @type available_updates: C{list} of C{dict}
    @ivar new_recommended:   the list of new recommended plugins available
    @type new_recommended:   C{list} of C{dict}
    """

    def __init__(self, available_updates, new_recommended):
        super(AvailablePluginUpdatesMessage, self).__init__()
        self.available_updates = available_updates
        self.new_recommended = new_recommended


class UpdateChecker(Loggable):

    """
    Helper Class for simple look up of updates on the remote elisa server.
    """

    # check each day, the value is in seconds
    check_interval = 86400

    def __init__(self, install_date, user_id, version, **extra_affiliation_params):
        super(UpdateChecker, self).__init__()

        # initial data
        self.install_date = install_date
        self.user_id = user_id
        self.version = version
        self.operating_system = misc.get_os_name()
        self.extra_affiliation_params = extra_affiliation_params

        self._pending_call = None
        self._current_dfr = None

    def parse_result(self, result):
        """
        Parse the given data into a dictionary. The syntax for result has to be
        a key-value pair per line separated by a colon (':'). Spaces at the
        beginning or the end are stripped from both the key and the value.
        For instance::

            foo: bar
            test: partial
            maybe : maybe not

        @param result: the result to parse
        @type result:  C{str}

        @return:    a dictionary containing the key-value pairs
        @rtype:     C{dict}
        """
        result_dict = {}
        for line in result.splitlines():
            try:
                key, value = line.split(':', 1)
            except ValueError:
                self.warning("Could not split '%s'" % line)
                continue

            result_dict[key.strip()] = value.strip()

        return result_dict

    def _get(self, uri):
        # This method is meant to be monkey-patched in unit tests.
        self.info("Retrieving page at %s", uri)
        return get_page(uri)

    def request(self):
        """
        Request for update data and parse it
        """
        request_params = {'user_id': quote(self.user_id),
                          'version': quote(self.version),
                          'date': quote(self.install_date),
                          'os': quote(self.operating_system)}
        request_params.update(self.extra_affiliation_params)

        request_url = UPDATES_URL % request_params

        dfr = self._get(request_url)
        dfr.addCallback(self.parse_result)
        return dfr

    def start(self, callback):
        """
        Sets up an automatic loop of update url calls starting right now.
        Everytime a result is received the callback is triggered with a
        dictionary of the parsed result as argument. The next iteration is done
        every L{check_interval}-seconds.

        @raises AlreadyRunning: if the method was already called before. It is only
        allowed to call this method once. It is mandatory to call L{stop} before
        calling start again.
        """
        if self._pending_call is not None or self._current_dfr is not None:
            raise AlreadyRunning("Don't call me twice")

        self._auto_request(callback)

    def _got_response(self, result, callback):
        if not result:
            self.warning("response of the Server was empty!")
            return

        dfr = callback(result)
        return dfr

    def _response_failed(self, failure):
        if failure.type != defer.CancelledError:
            self.warning("Problem when requesting for" \
                    "update: %s" % failure)

        # in any case, eat the error
        return None

    def _update_plugin_cache(self, result):
        plugin_registry = common.application.plugin_registry

        def get_updates_and_new_recommended_list(cache_file):
            plugins = \
                plugin_registry.get_downloadable_plugins(reload_cache=True)
            installed = list(plugin_registry.get_plugin_names())
            # hack for https://bugs.launchpad.net/elisa/+bug/341172
            if 'elisa-plugin-amp' not in installed:
                installed.append('elisa-plugin-amp')
            available_updates = []
            new_recommended = []

            def get_current_plugin_version(plugin_name):
                current = plugin_registry.get_plugin_by_name(plugin_name)
                if current is not None:
                    return LooseVersion(current.version)

                if plugin_name == 'elisa-plugin-amp':
                    # case of #341172: the plugin_registry didn't find amp,
                    # even though it is *always* installed, in that case, we
                    # know we have version 0.1, which didn't have its egg-info.
                    return LooseVersion('0.1')

            def iterate_plugins(plugins, installed,
                                available_updates, new_recommended):
                for plugin_dict in plugins:
                    plugin = Plugin.from_dict(plugin_dict)
                    if plugin.name not in installed and \
                        plugin_dict['quality'] == 'recommended' and \
                        plugin.runs_on_current_platform():
                            new_recommended.append(plugin_dict)
                    elif plugin.name in installed:
                        current_version = \
                            get_current_plugin_version(plugin.name)
                        if plugin.version > current_version:
                            available_updates.append(plugin_dict)
                    yield None

            def send_message(result, available_updates, new_recommended):
                if len(available_updates) > 0 or len(new_recommended) > 0:
                    msg = AvailablePluginUpdatesMessage(available_updates,
                                                        new_recommended)
                    common.application.bus.send_message(msg)

            updates_dfr = task.coiterate(iterate_plugins(plugins, installed,
                                                         available_updates,
                                                         new_recommended))
            updates_dfr.addCallback(send_message,
                                    available_updates, new_recommended)
            return updates_dfr

        def failed_update(failure):
            # Swallow all the errors.
            return None

        dfr = plugin_registry.update_cache()
        dfr.addCallback(get_updates_and_new_recommended_list)
        dfr.addErrback(failed_update)
        return dfr

    def _reset_pending_call(self, result, callback):
        pending = reactor.callLater(self.check_interval,
                self._auto_request, callback)
        self._pending_call = pending

    def _auto_request(self, callback):
        self._current_dfr = dfr = self.request()
        dfr.addCallback(self._got_response, callback)
        dfr.addErrback(self._response_failed)
        config = common.application.config
        should_update_plugin_cache = \
            config.get_option('update_plugin_cache', section='plugin_registry')
        if should_update_plugin_cache:
            dfr.addCallback(self._update_plugin_cache)
        dfr.addCallback(self._reset_pending_call, callback)

    def stop(self):
        """
        Stop any pending loop or http calls
        """
        try:
            self._pending_call.cancel()
        except (AttributeError, error.AlreadyCalled, error.AlreadyCancelled):
            pass

        try:
            self._current_dfr.cancel()
        except (AttributeError, defer.AlreadyCalledError):
            pass

        self._pending_call = None
        self._current_dfr = None
