# -*- 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.

from elisa.core.component import Component
from elisa.core.utils.i18n import install_translation
from elisa.core.utils import defer, notifying_list

from elisa.plugins.poblesec.base.hierarchy import HierarchyController
from elisa.plugins.poblesec.modal_popup import Popup
from elisa.plugins.pigment.widgets.const import STATE_SELECTED, STATE_LOADING

from twisted.internet import task, reactor

# Here for backward compatibility, all deferreds in Elisa should be cancellable
# (and thus use elisa.core.utils.defer).
from twisted.internet import defer as twisted_defer


_ = install_translation('poblesec')


class GenericListViewMode(Component):

    """
    Generic view mode API.

    It defines a common API for clients. All one has to do is inherit from this
    class and implement the following methods:

     - C{get_label(item)}
     - C{get_sublabel(item)}
     - C{get_default_image(item)}
     - C{get_image(item, theme)}
     - C{get_preview_image(item, theme)}
    """

    def get_label(self, item):
        """
        Return a text to display in a label to represent an item.

        This call is asynchronous, it should return a
        L{elisa.core.utils.cancellable_defer.CancellableDeferred} that, when
        triggered, returns the text of the label.

        @param item: a list item
        @type item:  a subclass of L{elisa.core.components.model.Model}

        @return:     a cancellable deferred
        @rtype:      L{elisa.core.utils.cancellable_defer.CancellableDeferred}
        """
        return defer.fail(NotImplementedError())

    def get_sublabel(self, item):
        """
        Return a text to display in a sublabel to represent an item.

        This call is asynchronous, it should return a
        L{elisa.core.utils.cancellable_defer.CancellableDeferred} that, when
        triggered, returns the text of the sublabel.

        @param item: a list item
        @type item:  a subclass of L{elisa.core.components.model.Model}

        @return:     a cancellable deferred
        @rtype:      L{elisa.core.utils.cancellable_defer.CancellableDeferred}
        """
        return defer.fail(NotImplementedError())

    def get_default_image(self, item):
        """
        Return the path of a theme resource to display as a default image for
        an item.

        @param item:  a list item
        @type item:   a subclass of L{elisa.core.components.model.Model}

        @return:      the path of a theme resource to display as a default
                      image for the item
        @rtype:       C{str}
        """
        raise NotImplementedError()

    def get_image(self, item, theme):
        """
        Return the path to an image file to display as an image for an item.

        This call is asynchronous, it should return a
        L{elisa.core.utils.cancellable_defer.CancellableDeferred} that, when
        triggered, returns the path to an image file on disk (downloaded and
        cached if necessary).

        If no other image than the default one is necessary/available, this
        method should return C{None}.

        @param item:  a list item
        @type item:   a subclass of L{elisa.core.components.model.Model}
        @param theme: the frontend's current theme
        @type theme:  L{elisa.plugins.pigment.widgets.theme.Theme}

        @return:      a cancellable deferred or C{None}
        @rtype:       L{elisa.core.utils.cancellable_defer.CancellableDeferred}
        """
        return defer.fail(NotImplementedError())

    def get_preview_image(self, item, theme):
        """
        Return the path to an image file to display as a preview image for an
        item.

        This call is synchronous, if no preview image is available yet for the
        item or if no other image than the default one is necessary, it should
        return C{None}.

        @param item:  a list item
        @type item:   a subclass of L{elisa.core.components.model.Model}
        @param theme: the frontend's current theme
        @type theme:  L{elisa.plugins.pigment.widgets.theme.Theme}

        @return:      the path to an image file on disk or C{None}
        @rtype:       C{str} or C{None}
        """
        raise NotImplementedError()

    def get_contextual_background(self, item):
        """
        Return the path to an image file to display as a contextual background
        for an item.

        This call is asynchronous, it should return a
        L{elisa.core.utils.cancellable_defer.CancellableDeferred} that, when
        triggered, returns the path to an image file on disk (downloaded and
        cached if necessary) or C{None} if not availadble.

        @param item:  a list item
        @type item:   a subclass of L{elisa.core.components.model.Model}

        @return:      a cancellable deferred
        @rtype:       L{elisa.core.utils.cancellable_defer.CancellableDeferred}
        """
        return defer.succeed(None)


class BaseListController(HierarchyController):

    """
    Base list controller with a common API for all list-like controllers.

    @ivar model: the data model
    @type model: L{elisa.core.utils.notifying_list.List}
    @ivar nodes: the list widget
    @type nodes: L{elisa.plugins.pigment.widgets.list.List}
    @ivar fastscroller: DOCME
    @type fastscroller: L{elisa.plugins.pigment.widgets.list.List}
    @ivar shortcuts: the fastscroller's model
    @type shortcuts: L{elisa.core.utils.notifying_list.List}

    @cvar fastscroller_enabled:   whether to show a fastscroller
                                  (C{False} by default)
    @type fastscroller_enabled:   C{bool}
    @cvar fastscroller_threshold: DOCME
    @type fastscroller_threshold: C{int}
    @cvar view_mode:              DOCME
    @type view_mode:              DOCME
    @cvar empty_label:  text shown when there no models are to
                        be displayed
    @type empty_label:  str
    """

    fastscroller_enabled = False
    fastscroller_threshold = 20
    view_mode = GenericListViewMode
    empty_label = None

    def __init__(self):
        super(BaseListController, self).__init__()
        self.model = notifying_list.List()
        self.shortcuts = notifying_list.List()
        self.nodes = None
        self.fastscroller = None
        self._default_action = None
        self._contextual_actions = []
        self._empty_alert_widget = None

    def initialize(self):
        dfr = super(BaseListController, self).initialize()

        # Contextual background's call later
        self._delayed_loading = None

        def populate(result):
            return self._populate()

        def create_actions(result):
            self._default_action, self._contextual_actions = \
                self.create_actions()

        def create_view_mode(result):
            return self.view_mode.create()

        def view_mode_created(view_mode):
            # FIXME: make self._view_mode public
            # (and rename view_mode to view_mode_cls)
            self._view_mode = view_mode

        def error_creating_view_mode(failure):
            self.warning('Error creating view mode: %s' % \
                         failure.getErrorMessage())
            return failure

        dfr.addCallback(populate)
        dfr.addCallback(create_actions)
        dfr.addCallback(create_view_mode)
        dfr.addCallbacks(view_mode_created, error_creating_view_mode)
        # Respect initialize()'s API that requires to return self
        dfr.addCallback(lambda result: self)
        return dfr

    def clean(self):
        if self._delayed_loading != None and self._delayed_loading.active():
            self._delayed_loading.cancel()
            self._delayed_loading = None
        if self.fastscroller is not None:
            self.fastscroller.disconnect_by_func(self._fastscroller_stated_changed)
            self.fastscroller.disconnect_by_func(self._shortcut_activated)
        self.nodes.disconnect_by_func(self._selected_item_cb)
        self.nodes.disconnect_by_func(self._item_activated_cb)
        self._stop_monitoring_model()
        self.model[:] = []
        return super(BaseListController, self).clean()

    def _populate(self):
        # Populate the model and the shortcuts accordingly if needed.

        def model_populated(model):
            self.model.extend(model)

        def error_populating_model(failure):
            self.warning('Error populating model: %s' % \
                         failure.getErrorMessage())
            return failure

        def populate_shortcuts(result):
            if self.fastscroller_enabled and \
                len(self.model) > self.fastscroller_threshold:
                return self._build_shortcuts()

        def shortcuts_populated(shortcuts):
            if shortcuts:
                self.shortcuts.extend(shortcuts)

        def error_populating_shortcuts(failure):
            self.warning('Error populating shortcuts: %s' % \
                         failure.getErrorMessage())
            return failure

        dfr = self.populate_model()
        dfr.addCallbacks(model_populated, error_populating_model)
        dfr.addCallback(populate_shortcuts)
        dfr.addCallbacks(shortcuts_populated, error_populating_shortcuts)
        return dfr

    def reload(self):
        """
        Reload the model as it is done at initialization time.
        Re-build accordingly the shortcuts.

        @return: a deferred fired when the reload is complete
        @rtype:  L{elisa.core.utils.defer.Deferred}
        """
        self._stop_monitoring_model()
        if self.shortcuts:
            self.shortcuts[:] = []
        if self.model:
            self.model[:] = []
        dfr = self._populate()
        dfr.addCallback(lambda result: self._start_monitoring_model())
        return dfr

    def populate_model(self):
        """
        Initial population of the data model (C{self.model}).

        This method should be overridden by subclasses. The default
        implementation returns an empty list.

        @return: a deferred fired when the population of the model is complete,
                 with the resulting model (of type C{list})
        @rtype:  L{elisa.core.utils.defer.Deferred}
        """
        return defer.succeed([])

    def create_actions(self):
        """
        Create the default action and the contextual actions associated to the
        type of item the controller will be presenting.

        The default implementation does create any action, subclasses should
        override this method.

        @return: a 2-tuple containing the default action and a list of
                 contextual actions
        @rtype:  (L{elisa.core.action.ContextualAction}, C{list}
                  of L{elisa.core.action.ContextualAction})
        """
        return None, []

    def set_frontend(self, frontend):
        super(BaseListController, self).set_frontend(frontend)

        # Create and render the list widget.
        self.nodes_setup()
        self.nodes.focus_before_activate = True
        self.nodes.connect('item-activated', self._item_activated_cb)
        self.nodes.connect('selected-item-changed', self._selected_item_cb)
        self.nodes.set_model(self.model)
        self.nodes.set_renderer(self.node_renderer)

        # By default the focus is forwarded to the list widget
        self.widget.set_focus_proxy(self.nodes)

        if self.shortcuts:
            self.fastscroller_setup()
            self.fastscroller.connect('item-activated',
                                      self._shortcut_activated)
            self.fastscroller.set_model(self.shortcuts)
            self.fastscroller.set_renderer(self.shortcut_renderer)
            self.fastscroller.connect('state-changed',
                                      self._fastscroller_stated_changed)

        # layout components (fastscroller and nodes)  
        self.layout_components()

    def ready(self):
        self._start_monitoring_model()

    def _monitor_model(self, *args):
        if self.empty_label:
            if len(self.model) == 0:
                self.display_empty_alert(self.empty_label)
            else:
                self.hide_empty_alert()

    def _start_monitoring_model(self):
        notifier = self.model.notifier
        notifier.connect('items-deleted', self._monitor_model)
        notifier.connect('items-inserted', self._monitor_model)
        # Initial check.
        self._monitor_model()

    def _stop_monitoring_model(self):
        notifier = self.model.notifier
        notifier.disconnect_by_func(self._monitor_model)
        notifier.disconnect_by_func(self._monitor_model)

    def nodes_setup(self):
        """
        Create the list widget.

        This method should be overridden by subclasses.
        """
        raise NotImplementedError()

    def fastscroller_setup(self):
        """
        Create fastscroller.

        This method should be overridden by subclasses.
        """
        raise NotImplementedError()

    def layout_components(self):
        """
        Layout fastscroller and nodes.

        This method should be overridden by subclasses.
        """
        pass

    def _build_shortcuts(self):
        shortcuts = []

        def iterate_model(shortcuts):
            for item in self.model:
                shortcut = self.get_shortcut_for_item(item)
                # we assume that self.model is sorted by shortcut
                if shortcut is not None and shortcut not in shortcuts:
                    shortcuts.append(shortcut)
                yield None

        dfr = task.coiterate(iterate_model(shortcuts))
        dfr.addCallback(lambda result: shortcuts)
        return dfr

    def get_shortcut_for_item(self, item):
        """
        @return: a shortcut for the item, or C{None}

        This method should be overridden by subclasses.
        """
        raise NotImplementedError()

    def shortcut_renderer(self, shortcut, widget):
        """
        DOCME

        This method should be overridden by subclasses.
        """
        raise NotImplementedError()

    def _get_item_index_for_shortcut(self, shortcut):
        # needs to be optimised caching {shortcut -> item index}
        for index, item in enumerate(self.model):
            if self.get_shortcut_for_item(item) == shortcut:
                return index

    def _shortcut_activated(self, widget, shortcut):
        item_index = self._get_item_index_for_shortcut(shortcut)
        self.nodes.selected_item_index = item_index

    def _get_shortcut_index_for_item_index(self, item_index):
        item = self.model[item_index]
        shortcut = self.get_shortcut_for_item(item)
        return self.shortcuts.index(shortcut)

    def _fastscroller_stated_changed(self, fastscroller, previous_state):
        if self.fastscroller.state == STATE_SELECTED:
            item_index = self.nodes.selected_item_index
            shortcut_index = self._get_shortcut_index_for_item_index(item_index)
            self.fastscroller.selected_item_index = shortcut_index

    def item_activated(self, item):
        """
        Callback invoked when an item is activated.

        The default implementation executes the default action associated with
        the item.

        This method should be overriden by subclasses that do not make use of
        contextual actions.

        @param item: the item that was activated
        @type item:  L{elisa.core.components.model.Model}

        @return: a deferred fired when the action taken is complete
        @rtype:  L{elisa.core.utils.defer.Deferred}
        """
        default_action = self._default_action
        if hasattr(item, 'default_action') and item.default_action is not None:
            default_action = item.default_action

        if default_action:
            return default_action.execute(item)

        # Handle gracefully the switch to the new API: fallback to the old API
        # if needed.
        try:
            result = self._node_clicked_proxy(None, item)
        except DeprecationWarning, error:
            self.warning(error.message)
            return defer.fail(item)
        except BaseException, error:
            # Catch any type of exception and wrap it in a deferred.
            return defer.fail(error)
        else:
            if isinstance(result, twisted_defer.Deferred):
                return result
            else:
                # Artificially wrap the result (if any) in a deferred
                return defer.succeed(result)

    def _do_item_activated(self, item):
        # TODO: cancel a potential previous call from another item
        widget = self.nodes._widget_from_item_index(self.model.index(item))
        previous_state = widget.state
        widget.state = STATE_LOADING

        def reset_widget_state(result_or_failure, widget, previous_state):
            widget.state = previous_state

        dfr = self.item_activated(item)
        dfr.addBoth(reset_widget_state, widget, previous_state)
        return dfr

    def _item_activated_cb(self, widget, item):
        self._do_item_activated(item)

    def node_clicked(self, widget, item):
        """
        [DEPRECATED] Callback invoked when an item of the list representing a
        given level of the hierarchy is clicked.

        @deprecated:   implement C{item_activated} instead

        @param widget: the selected list item widget in the view
        @type widget:  L{elisa.plugins.pigment.widgets.widget.Widget}
        @param item:   the selected list item in the controller's model
        @type item:    L{elisa.core.components.model.Model}
        """
        msg = 'This interface is deprecated, ' \
              'please implement item_activated instead'
        raise DeprecationWarning(msg)

    def _node_clicked_proxy(self, widget, item):
        """
        [DEPRECATED] This method will be removed once we manage to completely
        unify the way items are activated (mouse, keyboard, remote control)
        using the item-activated signal.

        This method is triggered by the widget item-clicked signal. It figures
        out the widget that was really clicked, checks if it is still in a
        previous_clicked mode or not sensitive to clicks. If we should react it
        calls self.node_clicked (the public method) with the selected widget as
        the first parameter and the item as the second parameter.
        """
        if not self.sensitive:
            return

        selected_widget = \
            self.nodes._widget_from_item_index(self.model.index(item))

        return self.node_clicked(selected_widget, item)

    def stop_loading_animation(self):
        """
        [DEPRECATED] This method can be safely removed once the transition from
        using node_clicked to implementing item_activated is complete.
        """
        msg = 'This method is deprecated. ' \
              'The base list controller now handles ' \
              'the loading state of widgets on its own.'
        raise DeprecationWarning(msg)

    def sensitive_set(self, value):
        self._sensitive = value

    def sensitive_get(self):
        return self._sensitive

    sensitive = property(fget=sensitive_get, fset=sensitive_set)

    def display_empty_alert(self, label):
        """
        Display an alert widget to inform that the model is empty.

        @param label: the text of the alert
        @type label:  C{unicode}
        """
        if self._empty_alert_widget is not None:
            # There is already an empty alert displayed.
            return

        def go_back():
            browser = self.frontend.retrieve_controllers('/poblesec/browser')[0]
            browser.history.go_back()

        title = _('EMPTY SECTION')
        subtitle = label
        text = ''
        buttons = [(_('Back'), go_back)] 

        self._empty_alert_widget = Popup(title, subtitle, text, buttons)
        self._empty_alert_widget.set_name('empty_alert')
        self._empty_alert_widget.visible = True
        self.widget.add(self._empty_alert_widget)
        self.widget.set_focus_proxy(self._empty_alert_widget)
        if self.widget.focus:
            self._empty_alert_widget.set_focus()

    def hide_empty_alert(self):
        """
        Hide the alert widget that informs that the model is empty.
        """
        if self._empty_alert_widget is None:
            # The empty alert is not displayed.
            return

        self.widget.remove(self._empty_alert_widget)
        self._empty_alert_widget = None

    def node_renderer(self, item, widget):
        """
        Render an item in a list widget.

        The default implementation does nothing. Subclasses should override
        this method to control how items are visually rendered.

        @param item:   the item to render
        @type item:    L{elisa.core.components.model.Model}
        @param widget: the widget in which to render the item
        @type widget:  L{elisa.plugins.pigment.widgets.widget.Widget}
        """
        pass

    def node_selected(self, widget, item, previous_item):
        """
        Callback invoked when an item is selected.

        The default implementation does nothing.
        Subclasses should override if an action is to be taken.

        @param widget:        the list widget
        @type widget:         L{elisa.plugins.pigment.widgets.list.List}
        @param item:          the newly selected item
        @type item:           L{elisa.core.components.model.Model}
        @param previous_item: the previously selected item
        @type previous_item:  L{elisa.core.components.model.Model}
        """
        pass

    def _selected_item_cb(self, widget, item, previous_item):
        self.node_selected(widget, item, previous_item)
        self._schedule_load_background()
        self._update_logo(item)

    def _update_logo(self, selected_item):
        try:
            source_icon = selected_item.source_icon
        except AttributeError:
            pass
        else:
            browser = self.frontend.retrieve_controllers('/poblesec/browser')[0]
            browser.set_source_logo(source_icon)

    def _schedule_load_background(self):
        # plan on loading the contextual background after a certain delay
        # reset it if it was already counting down

        # FIXME: hardcoded delay
        delay = 1.0
        if self._delayed_loading != None and self._delayed_loading.active():
            self._delayed_loading.reset(delay)
        else:
            self._delayed_loading = reactor.callLater(delay,
                                                      self._load_background)

    def _load_background(self):
        # load a visual representation of the currently selected item
        selected_item = self.model[self.nodes.selected_item_index]

        def contextual_background_retrieved(background_path):
            if background_path != None:
                main = self.frontend.retrieve_controllers('/poblesec')[0]
                main.background.load_file(background_path)

        dfr = self._view_mode.get_contextual_background(selected_item)
        dfr.addCallback(contextual_background_retrieved)
