# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2007-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>
#          Alessandro Decina <alessandro@fluendo.com>

from twisted.internet import reactor, defer
from twisted.internet.error import ConnectionDone, ConnectionLost
import os

from elisa.core import common
from elisa.core.component import Component
from elisa.core.log import Loggable, getFailureMessage
from elisa.core.utils.misc import chainDeferredIgnoringResult
from elisa.core.utils import locale_helper
from elisa.plugins.gstreamer.request import RequestQueue
from elisa.plugins.gstreamer.gst_metadata import able_to_handle, \
        supported_schemes, supported_keys
from elisa.plugins.amp.master import Master, SlaveProcessProtocol
from elisa.plugins.gstreamer.amp_protocol import GetMetadata, \
        GstMetadataMasterFactory, AddGstPaths
from elisa.plugins.gstreamer.pipeline import get_thumbnail_location 
from elisa.core.utils import path_utils

import gst

# use Loggable as the first base so that its debugging methods are used
class GstMetadataSlaveProcessProtocol(Loggable, SlaveProcessProtocol):
    """SlaveProcessProtocol that uses Loggable methods to log"""
    def __init__(self, master, slave_cookie):
        SlaveProcessProtocol.__init__(self, master, slave_cookie)
        Loggable.__init__(self)

    def __str__(self):
        return SlaveProcessProtocol.__str__(self)

class GstMetadataMaster(Loggable, Master):
    """
    DOCME

    @ivar running:  Whether the metadata master and is running and processing
                    requests.
    @type running:  C{bool}
    """
    serverFactory = GstMetadataMasterFactory
    slaveProcessProtocolFactory = GstMetadataSlaveProcessProtocol 
    socket_prefix = 'elisa-metadata-'

    def __init__(self, max_restart_retries,
                 slave_spawn_timeout, slave_retry_timeout_step):
        Loggable.__init__(self)
        Master.__init__(self, 
                slave_runner='elisa.plugins.gstreamer.amp_slave.run_slave')
        self._requests = RequestQueue()
        self._restarted = 0
        self._next_request_call = None
        self._in_request = False
        self._process_next_request = None
        self._restart_defer = None
        self._last_slave_death_cause = None

        self.max_restart_retries = max_restart_retries
        self.spawn_timeout = slave_spawn_timeout
        self.retry_spawn_timeout_step = slave_retry_timeout_step
        self.running = False

    def _get_existing_thumbnail(self, metadata):
        """
        Fill the metadata structure with the thumbnail path if the thumbnail
        for this uri already exists

        @param metadata: A dictionnary of the metadata that are currently
                            being queried
        @type  metadata: C{dict}

        @return: A deferred that will be fired once we have an existing
        thumbnail uri or know we can't have one
        @rtype:  C{twisted.internet.defer.Deferred}
        """
        if not 'thumbnail' in metadata.keys():
            return defer.succeed(False)

        unicode_uri = unicode(metadata['uri'])
        # we reproduce the situation in which the slave is, to be sure to have
        # the same results. For now, the slave has a string in gstreamer file
        # encoding
        locale_uri = unicode_uri.encode(locale_helper.gst_file_encoding())
        thumbnail_path = get_thumbnail_location(locale_uri)
        dfr = path_utils.have_file(thumbnail_path)
        def set_thumbnail(result, thumbnail_path):
            if result:
                # we use pgm_file_encoding for consistency with what is done
                # in the slave
                thumbnail_path = \
                    thumbnail_path.encode(locale_helper.pgm_file_encoding())
                metadata['thumbnail'] = thumbnail_path
            return result
        dfr.addCallback(set_thumbnail, thumbnail_path)
        return dfr

    def get_metadata(self, metadata):
        self.debug("get_metadata() for %s" % metadata)
        # fast path for deleted local files
        dfr = self._check_deleted_file(metadata)
        if dfr is not None:
            return dfr

        # fast path for already existing thumbnails
        dfr = self._get_existing_thumbnail(metadata)

        def enqueue(result):
            if None not in metadata.itervalues():
                # We don't need to ask anything from the slave
                self.debug("Don't need to do anything more for %s" % metadata)
                return defer.succeed(metadata)

            self.debug("enqueuing request for %s" % metadata)
            dfr = self._requests.enqueue(metadata)
            if not self._in_request:
                self._schedule_next_request()

            return dfr

        dfr.addCallback(enqueue)
        return dfr

    def startSlaves(self, num, spawn_timeout=None):
        def return_result(result, real_result):
            return real_result

        def send_paths(result):
            paths = gst.registry_get_default().get_path_list()
            box = [{'path': path} for path in paths]
            deferreds = []
            for slave in self._slaves.itervalues():
                dfr = slave.amp.callRemote(AddGstPaths, paths=box)
                deferreds.append(dfr)

            dfr = defer.DeferredList(deferreds)
            dfr.addCallback(return_result, result)
            
            return dfr

        try:
            dfr = Master.startSlaves(self, num,
                                     spawn_timeout or self.spawn_timeout)
        except TypeError:
            # This is to work around
            # https://bugs.launchpad.net/elisa/+bug/341172. In a nutshell,
            # there are cases where we might have an old amp plugin.
            dfr = Master.startSlaves(self, num)

        def set_running(result):
            self.running = True
            return result

        # We always want to be set as running even if the slave creation
        # failed (in that case, we count on the same mechanism as what is used
        # to relaunch the slave when it broke in other cases than when
        # starting)
        dfr.addBoth(set_running)
        dfr.addCallback(send_paths)

        return dfr

    def stop(self):
        self.running = False
        if self._next_request_call is not None:
            self._next_request_call.cancel()
            self._next_request_call = None
        return Master.stop(self)

    def _check_deleted_file(self, metadata):
        try:
            uri = metadata['uri']
        except KeyError:
            return None
        
        if uri.scheme == 'file' and \
                not os.path.exists(uri.path):
            return defer.fail(Exception('file not found %s' % uri))

        return None

    def _schedule_next_request(self):
        if self._restart_defer or self._next_request_call:
            return

        assert self._next_request_call is None
        self._next_request_call = reactor.callLater(0, self._next_request)

    def _next_request(self):
        assert not self._in_request
        if self._next_request_call is not None and self._next_request_call.active():
            self._next_request_call.cancel()
        self._next_request_call = None

        #FIXME: Experimentally, we seem to need to check both if we are
        # running and if the application is running. This shouldn't be needed.
        if not self.running or not common.application.running:
            self.warning("Not handling request as we are not in running mode")
            return

        try:
            slave = self._slaves.values()[0]
        except IndexError:
            self.warning('no slave running in _next_request')
            # no slave running, we try to restart one first
            self._try_restart_slave()
            return

        try:
            request = self._requests.dequeue()
        except IndexError:
            self.debug('metadata queue empty')
            return

        # a MediaUri should always be convertible to unicode
        unicode_uri = unicode(request.metadata['uri'])
        # we can convert that to the encoding expected by gstreamer
        uri = unicode_uri.encode(locale_helper.gst_file_encoding())

        self.debug('starting request %s' % request.metadata)

        keys = [{'key': key} for key in request.metadata.keys()
                if key != 'uri']

        dfr = slave.amp.callRemote(GetMetadata,  uri=uri, metadata=keys)
        self._in_request = True
        dfr.addCallback(self._get_metadata_cb, request)
        dfr.addErrback(self._get_metadata_eb, request)
    
    def _get_metadata_cb(self, result, request):
        self._in_request = False

        for dic in result['metadata']:
            request.metadata[dic['key']] = dic['value']

        self._schedule_next_request()
        request.defer.callback(request.metadata)
    
    def _restart_callback(self, result):
        self._restart_defer = None
        self._restarted = 0
        self._last_slave_death_cause = None
        self._schedule_next_request()

    def _restart_errback(self, failure):
        self._restart_defer = None
        self._last_slave_death_cause = failure
        
        self.info('could not restart slave %s' % getFailureMessage(failure))
        self._try_restart_slave()
        return

    def _get_metadata_eb(self, failure, request):
        self._in_request = False

        self.debug('get metadata failed %s %s' %
                (request.metadata, getFailureMessage(failure)))

        if failure.check(ConnectionDone, ConnectionLost) and self._slaves_num:
            # FIXME: according to the Twisted Documentation we should not receive a
            # ConnectionLost in the case that the slave dies but it happens on
            # windows systems. For now we restart in any case but later we should
            # find out why it happens in the first place and fix it there.
            self._try_restart_slave()
        else:
            self._schedule_next_request()

        # errback this request so that we don't continue crashing on the same
        # file forever
        if not request.cancelled:
            request.defer.errback(failure)
    
    def _try_restart_slave(self):
        if self._restarted == self.max_restart_retries:
            self.warning('slave dead %s times, not trying anymore',
                    self._restarted)

            self._abort_pending_requests(self._last_slave_death_cause)
            return

        self.warning('restarting slave')
        self._restarted += 1
        spawn_timeout = self.spawn_timeout + \
                        self.retry_spawn_timeout_step * self._restarted
        self._restart_defer = self.startSlaves(1, spawn_timeout)
        self._restart_defer.addCallback(self._restart_callback)
        self._restart_defer.addErrback(self._restart_errback)
    
    def _abort_pending_requests(self, failure):
        self.warning('aborting all requests, failure: %s' %
                    getFailureMessage(failure))
        while True:
            try:
                request = self._requests.dequeue()
            except IndexError:
                break

            if not request.cancelled:
                request.defer.errback(failure)

class GstMetadataAmpClient(Component):

    default_config = \
        {'max_restart_retries': 10,
         'slave_spawn_timeout': 7,
         'slave_retry_timeout_step': 2}

    config_doc = \
        {'max_restart_retries': 'How many times to try and restart a dead ' \
            'slave before giving up.',
         'slave_spawn_timeout': 'Timeout in seconds before killing a slave ' \
            'that is not responding.',
         'slave_retry_timeout_step': 'Step by which to increase the slave ' \
            'spawn timeout (in seconds) between each new attempt.'}

    def initialize(self):
        def start_slaves_cb(result_or_failure):
            self._start_dfr = None
            return self

        self._master = \
            GstMetadataMaster(self.config['max_restart_retries'],
                              self.config['slave_spawn_timeout'],
                              self.config['slave_retry_timeout_step'])
        self._master.start()
        self._start_dfr = dfr = self._master.startSlaves(1)
        dfr.addBoth(start_slaves_cb)

        return super(GstMetadataAmpClient, self).initialize()

    def clean(self):
        if self._start_dfr is not None:
            return self._call_after_start_dfr(self.clean)

        def parent_clean(result):
            return super(GstMetadataAmpClient, self).clean()
        
        def stop_slaves_cb(result):
            dfr = self._master.stop()
            dfr.addCallback(lambda result: self)

            return dfr

        dfr = self._master.stopSlaves()
        dfr.addCallback(stop_slaves_cb)
        dfr.addBoth(parent_clean)

        return dfr

    def get_rank(self):
        return 10

    def able_to_handle(self, metadata):
        return able_to_handle(supported_schemes,
                supported_keys, metadata)

    def _call_after_start_dfr(self, method, *args, **kw):
        def call_after(result):
            return method(*args, **kw)

        dfr = defer.Deferred()
        dfr.addCallback(call_after)
        chainDeferredIgnoringResult(self._start_dfr, dfr)
        return dfr

    def get_metadata(self, metadata):
        if self._start_dfr is not None:
            return self._call_after_start_dfr(self.get_metadata, metadata)

        return self._master.get_metadata(metadata)
