# -*- coding: utf-8 -*-
# Elisa - Home multimedia server
# Copyright (C) 2006-2008 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 Elisa with Fluendo's plugins.
#
# The GPL part of Elisa is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Elisa" in the root directory of this distribution package
# for details on that license.


__maintainer__ = 'Florian Boucault <florian@fluendo.com>'


from elisa.core import common
from elisa.core import manager
from elisa.core import input_event
from elisa.core.utils import signal, classinit
from elisa.base_components.input_provider import PushInputProvider, PollInputProvider
from twisted.internet import reactor

class InputManager(manager.Manager):
    """
    InputManager provides a common place to retrieve input events
    coming from a GUI toolkit, additional input sources or even a
    network. InputEvents can be pushed by
    L{elisa.base_components.input_provider.InputProvider}s
    or be polled by the InputManager, depending on the implementation
    chosen: L{elisa.base_components.input_provider.PollInputProvider}
    or L{elisa.base_components.input_provider.PushInputProvider}.

    Other objects can connect to the manager's signals
    that are emitted when L{elisa.core.input_event.InputEvent}s coming from
    L{elisa.base_components.input_provider.InputProvider}s are received.
    """

    def __init__(self):
        manager.Manager.__init__(self)

        # 35Hz polling rate for the PollInputProvider
        self._polling_rate = 1 / 35.
        self._polling = False
        self._poll_call = None

    def start(self):
        """ Start a polling loop that will regularly check the registered
        L{elisa.base_components.input_provider.PollInputProvider}s for new
        L{elisa.core.input_event.InputEvent}s.
        """
        self.info("Starting")
        self._polling = True
        # start a looping callLater
        self._poll_call = reactor.callLater(self._polling_rate,
                                            self._poll_events)

    def stop(self):
        """ Clean all the registered
        L{elisa.base_components.input_provider.InputProvider}s and stop
        polling for new L{elisa.core.input_event.InputEvent}s.
        """
        self.info("Stopping")
        if self._poll_call:
            try:
                self._poll_call.cancel()
            except:
                pass
        self._polling = False
        manager.Manager.stop(self)

    def process_event(self, event, provider_path):
        """ Fire the signal corresponding to the event.

        Each event type is mapped to a signal instance to which other
        elisa components can connect (e.g to monitor user key presses).

        This method can be called by
        L{elisa.base_components.input_provider.PushInputProvider}
        components when they receive input data from the input device.

        @param event:         the event to process
        @type event:          L{elisa.core.input_event.InputEvent}
        @param provider_path: the path of the InputProvider where the event is from
        @type provider_path:  str
        """
        self.debug("Event received: %r", event)

        if event == None:
            return

        self._emit_event(event, provider_path)

    def register_component(self, component):
        """ Register a new InputProvider in the InputManager so that the
        events collected by the former are propagated by the latter.

        @param component: the InputProvider instance to register
        @type component:  L{elisa.base_components.input_provider.InputProvider}
        """
        manager.Manager.register_component(self, component)
        # register myself to push_input_providers
        if isinstance(component, PushInputProvider):
            component.input_manager = self
            component.bind()
        sig_name = 'sig_%s' % component.path
        setattr(self, sig_name, signal.Signal(sig_name,
                                              input_event.InputEvent))

    def unregister_component(self, component):
        """ Clean the InputProvider and unregister it from the InputManager;
        no events from the InputProvider will be propagated anymore.

        @param component: the InputProvider instance to unregister
        @type component:  L{elisa.base_components.input_provider.InputProvider}
        """
        manager.Manager.unregister_component(self, component)
        component.clean()

    def subscribe(self, provider_path, callback):
        """ Register a callback which will be called whenever an
        L{elisa.core.input_event.InputEvent} coming from the given 
        L{elisa.base_components.input_provider.InputProvider} is raised.

        The first argument passed to the callback will be a
        L{elisa.core.input_event.InputEvent} instance.

        @param provider_path: the path of the InputProvider from which the
                              events will come from
        @type provider_path:  str
        @param callback:      the object to be called when an event is raised
        @type callback:       callable
        """
        sig_name = 'sig_%s' % provider_path
        try:
            sig = getattr(self, sig_name)
        except AttributeError:
            msg = "Couldn't connect to input provider %r. Signal %r not found"
            self.debug(msg, provider_path, sig_name)
        else:
            sig.connect(callback)

    def unsubscribe(self, provider_path, callback):
        """ Unregister a previously registered callback against a given
        L{elisa.base_components.input_provider.InputProvider}. Events will no
        longer be forwarded to the callback.

        @param provider_path: the path of the InputProvider from which the
                              events used to come from
        @type provider_path:  str
        @param callback:      the already registered callback
        @type callback:       callable
        """
        sig_name = 'sig_%s' % provider_path
        try:
            sig = getattr(self, sig_name)
        except AttributeError:
            self.debug("Couldn't disconnect from input provider %r."\
                       " Signal %r not found", provider_path, sig_name)
        else:
            if sig:
                try:
                    sig.disconnect(callback)
                except Exception, exc:
                    self.debug(exc)

    def _poll_events(self):
        """ Poll each registered PollInputProvider for InputEvents to
        process.
        """
        input_providers = filter(lambda p: isinstance(p, PollInputProvider),
                                 self._components)

        for input_provider in input_providers:
            events = input_provider.get_input_events()
            for event in events:
                self.process_event(event, input_provider.path)

        if self._polling:
            self._poll_call = reactor.callLater(self._polling_rate,
                                                self._poll_events)

    def _emit_event(self, event, provider_path):
        """ Emit an event coming from a given InputProvider """
        sig_name = 'sig_%s' % provider_path
        try:
            sig = getattr(self, sig_name)
        except AttributeError:
            self.debug("Input provider signal %r not found", sig_name)
        else:
            try:
                sig.emit(event)
            except:
                fail_path = common.application.log_traceback()
                self.warning("Emitting Signal %s failed. A full traceback"\
                             " can be found at %s" % (sig_name, fail_path) )
