# -*- 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__ = 'Benjamin Kampmann <benjamin@fluendo.com>'

from menu_entry_builder import MenuEntryBuilder
from elisa.core.observers.list import ListObserver, ListObservable
from elisa.core.observers.dict import DictObserver, DictObservable
from elisa.core.media_uri import MediaUri
from elisa.core.media_manager import MediaProviderNotFound
from elisa.core import common

from Queue import Queue

from weakref import ref

from twisted.internet import defer, reactor 
from twisted.internet.error import AlreadyCalled

class DeferredCanceled(Exception):
    pass

class MetadataObserver(DictObserver):

    def __init__(self, model):
        DictObserver.__init__(self)
        self._model = ref(model)

    def modified(self, key, value):
        if key == 'default_image':
            model = self._model()
            if model:
                model.thumbnail_source = value
                
class UriObserver(ListObserver):

    def __init__(self, model, config, first_element_dfr):
        ListObserver.__init__(self)
        self._model = ref(model)
        self._config = config
        self._queue = Queue()
        self._observers = []
        self.running = False
        self.check_on_empty = False
        self._first_element_dfr = first_element_dfr
        self._queue_delayed_call = None

    def stop(self):
        """
        clean up all references and stop the process of running
        """
        self.running = False
        try:
            self._first_element_dfr.errback(DeferredCanceled)
        except defer.AlreadyCalledError:
            pass

        if self._queue_delayed_call and self._queue_delayed_call.active():
            self._queue_delayed_call.cancel()
        model = self._model()
        if model:
            model.loading = False
        self._observers[:] = []

    def inserted(self, elements, position):
        model = self._model()

        if not model:
            return

        for (counter, element) in enumerate(elements):
            uri, metadata = element

            new_model = model.activity.create_menu_for_uri(uri, self._config)
            self._queue.put( (uri, metadata, new_model, position+counter) )

        if not self.running:
            self.running = True
            self._process_queue()

    def _process_queue(self):
        # FIXME: Does that need to be threadsafe?
        def delay_call(result):
            self._queue_delayed_call = reactor.callLater(0.01, self._process_queue)

        if not self._queue.empty():
            uri, metadata, model, index = self._queue.get()
            if self.running:
                dfr = self._process_model(uri, metadata, model, index)
                dfr.addBoth(delay_call)
        else:
            self.running = False
            model = self._model()
            if model and self.check_on_empty and len(model.children) == 0:
                try:
                    self._first_element_dfr.errback(Exception)
                except defer.AlreadyCalledError:
                    pass
                model = self._model()
                if model:
                    model.loading = False

    def _process_model(self, uri, metadata, model, index):
        registry = common.application.plugin_registry
        metadata_manager = common.application.metadata_manager
        media_manager = common.application.media_manager

        def observe_metadata(metadata, model):
            if not isinstance(metadata, DictObservable):
                # metadata is pretty much never a DictObservable
                metadata = DictObservable(metadata)

            content_type = self._config['ContentType']

            # FIXME: resetting default_image works only when dealing with
            #        audio content_type
            if content_type in ('audio','artist', 'album') and \
                   not metadata.has_key('default_image'):
                metadata['default_image'] = None
                
            metadata['uri'] = uri
            metadata['content-type'] = content_type
                
            metadata_observer = MetadataObserver(model)
            metadata.add_observer(metadata_observer)
            self._observers.append(metadata_observer)
            return metadata
        
        def insert_model(new_model, index):
            model = self._model()
            if not model:
                return
            new_model.activity = model.activity
            model.children.insert(index, new_model)
            model.has_children = True
            try:
                model.loading = False
                self._first_element_dfr.callback(model.children)
            except AlreadyCalled:
                # the callback has to be fired only for the very first
                # insertion
                pass
            
        def set_play_action(uri, model, file_type):
            action = registry.create_component('xmlmenu:play_action')
            action.player_model = model.activity.activity.player_model
            action.model = ref(model)
            model.activate_action = action

        def set_videoplay_action(uri, model):
            action = registry.create_component('xmlmenu:videoplay_action')
            action.player_model = model.activity.activity.player_model
            action.model = ref(model)
            model.activate_action = action

        def set_enqueue_action(model):
            registry = common.application.plugin_registry
            action = registry.create_component('xmlmenu:enqueue_action')
            action.parent_model = model
            #FIXME: pfui!
            action.playlist_model = model.activity.activity.player_model.playlist
            model.children = registry.create_component('base:list_model')
            new_model = registry.create_component('base:menu_node_model')
            new_model.text = 'Enqueue'
            new_model.activate_action = action
            model.children.append(new_model)

        def set_thumbnail_source(uri, metadata, model, file_type):
            if metadata.has_key('default_image'):
                model.thumbnail_source = metadata['default_image']
            else:
                # thumbnail source is the media itself for everything but
                # directories and audio files
                if file_type == "image" or file_type == "video":
                    model.thumbnail_source = uri
                else:
                    model.thumbnail_source = None

        def got_media_type(media_type):
            file_type = media_type['file_type']
            model.file_type = file_type
            if file_type == "directory":
                model.has_children = True

                # FIXME: hack to detect the URI represents an audio album.
                #        That totally suck, hardcoding a theme icon.
                if uri.scheme == 'elisa' and uri.path.find('/albums/') != -1:
                    theme_icon = 'audio_album_icon'
                else:
                    theme_icon = self._config['DefaultDirsIcon']
                    
                model.theme_icon = theme_icon
                # Disabled, because it is hakkish
                #set_enqueue_action(model)
            else:
                # FIXME: action management should not be hardcorded!
                model.has_children = False
                model.theme_icon = self._config['DefaultFilesIcon']
                if file_type == "video":
                    set_videoplay_action(uri, model)
                elif file_type == "audio":
                    set_play_action(uri, model, file_type)

            file_filter = self._config['MediaFilter']

            if file_filter.lower() == 'any' or \
                         file_type == 'directory' or \
                         file_type == file_filter:

                # FIXME: this is an ugly hack...
                if metadata.has_key('artist'):
                    model.sub_text = metadata['artist']

                modified_metadata = observe_metadata(metadata, model)
                metadata_manager.get_metadata(modified_metadata)
                set_thumbnail_source(uri, modified_metadata, model, file_type)
                insert_model(model, index)

        dfr = media_manager.get_media_type(uri)
        dfr.addCallback(got_media_type)
        return dfr

    def removed(self, elements, position):
        model = self._model()

        if not model:
            return

        index = model.activity.files_index_start+position
        model.children[index:index+len(elements)] = []

        if len(model.children) == model.activity.files_index_start:
            model.children[:] = []
            model.has_children = False

class UriNodeBuilder(MenuEntryBuilder):
    """
    Build MenuEntries for the type 'menu_node'
    """

    def initialize(self):
        MenuEntryBuilder.initialize(self)
        self._observers = {}

    def menu_entry_identifiers__get(self):
        return ['uri_node']

    def clean(self):
        for observer in self._observers.itervalues():
            observer.stop()

    def build_menu_entry(self, parent, node):
        # URI-Lookup
        url = node.find('URI')
        if url == None:
            self.warning("URI-MenuNode needs a URI!")
            return defer.succeed(parent.children)
        uri = MediaUri(url.text)

        # Label-Making
        label_tag = node.find('Label')
        label = self._make_label(label_tag)

        default_config = self.model_configs[parent]
        config = self._make_config(node.find('Configuration'),
                                    default_config)

        registry = common.application.plugin_registry
        menu_model = registry.create_component('base:menu_node_model')
        menu_model.has_children = True
        menu_model.text = label
        menu_model.activity = self
        menu_model.uri = uri
        
        self._set_icon(menu_model, node.find('Icon'),
                       fallback=config['DefaultDirsIcon'])

        self.model_configs[menu_model] = config

        parent.children.append(menu_model)
        self.debug("appending %s to %s" % (menu_model.text, parent.text))

        return defer.succeed([menu_model])

    def unload(self, model):
        model_id = id(model)
        if self._observers.has_key(model_id):
            self._observers[model_id].stop()
            del self._observers[model_id]
    
    def loadmore(self, model):
        self.debug("got loadmore for %s" % model.text)
        uri = model.uri
        config = self.model_configs[model]

        if model.children == None:
            registry = common.application.plugin_registry
            model.children = registry.create_component('base:list_model')
            model.children.content_type = config['ContentType']

        model.children.activity = self
        self.debug("set activity to self")

        if len(model.children) > 0:
            return defer.succeed(model.children)

        def got_children(children, uri, model, dfr, observer):
            model.has_children = (len(children) > 0)
            # FIXME: another ugly hack and breaks on AudioCD
            if observer.running:
                observer.check_on_empty = True
            else:
                try:
                    dfr.errback(Exception)
                except defer.AlreadyCalledError:
                    pass

                model.loading = False
            return model.children

        def erroneous_get_children(error, uri, model):
            model.has_children = False
            model.loading = False
            path = common.application.log_failure(error)
            self.warning("Error while retrieving children of %s. A full"\
                         " traceback can be found at %s" % (uri, path))
            return model.children

        first_element_dfr = defer.Deferred()
        if uri:
            children = ListObservable()
            uri_observer = UriObserver(model, config, first_element_dfr)
            children.add_observer(uri_observer)
            self._observers[id(model.children)] = uri_observer

            model.has_children = False
            model.loading = True
            media_manager = common.application.media_manager
            try:
                dfr = media_manager.get_direct_children(uri, children)
            except MediaProviderNotFound, exc:
                # FIXME: we should return a kind of message to the user
                self.warning("No Media provider found for %s" % uri)
                model.loading = False
                return defer.fail(exc)

            if isinstance(dfr, defer.Deferred):
                dfr.addCallback(got_children, uri, model,
                                first_element_dfr,
                                uri_observer)
                dfr.addErrback(erroneous_get_children, uri, model)
            else:
                model.has_children = False
                model.loading = False
        else:
            self.debug("No URI for %r", model)
            return defer.fail(None)
        return first_element_dfr

    def create_menu_for_uri(self, uri, config):
        label = uri.label
        registry = common.application.plugin_registry
        # This might returns a deferred!
        menu_model = registry.create_component('base:menu_node_model')
        menu_model.uri = uri
        menu_model.text = uri.label
        menu_model.activity = self
        self.model_configs[menu_model] = config
        return menu_model
 
