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

"""
UPnP MediaServer
"""

__maintainer__ = "Philippe Normand <philippe@fluendo.com>"

from elisa.base_components import service_provider
from elisa.core import media_uri, log
from elisa.core import common, component
from elisa.core.observers.list import ListObserver
from elisa.core.utils import misc, classinit
from elisa.core.application import ComponentsLoadedMessage
from twisted.internet import defer
from twisted.web import resource, static, server
from twisted.spread import pb
import mimetypes, threading
import urllib

plugin_registry = common.application.plugin_registry
CoherencePluginMessage = plugin_registry.get_component_class('coherence:coherence_plugin_message')
PBReferenceableMessage = plugin_registry.get_component_class('twisted:pbreferenceable_message')
HTTPResourceMessage = plugin_registry.get_component_class('httpd:httpresource_message')
LocationsListMessage = plugin_registry.get_component_class('xmlmenu:locations_list_message')

MediaDB = plugin_registry.get_component_class('media_db:db')


class LocationsObserver(ListObserver):

    def __init__(self, media_server):
        ListObserver.__init__(self)
        self._media_server = media_server

    def inserted(self, elements, position):
        for location in elements:
            self._media_server.add_media_location(location)

    def removed(self, elements, position):
        for element in elements:
            self._media_server.remove_media_location(location)

class UPnPResource(object, resource.Resource):
    """
    DOCME
    """


    __metaclass__ = classinit.ClassInitMeta
    __classinit__ = classinit.build_properties

    addSlash = True

    def __init__(self, upnp_ms):
        resource.Resource.__init__(self)
        self._upnp_ms = upnp_ms

    def render(self, request):
        return """
        <html>
        <head>
        <title>Elisa UPnP page</title>
        </head>
        <body>
        <h1>UPnP page!</h1>
        </body>
        </html>
        """

    def getChild(self, path, request):
        child = self
        if path != '':
            #media_manager = common.application.media_manager
            try:
                media_id = int(path)
            except ValueError:
                log.debug('upnp_resource', "wrong media ID: %r", path)
            else:
                db = MediaDB.get_connection()
                cm = db.get_media_with_id(int(path))
                uri = media_uri.MediaUri(cm.uri)
                f = uri.host + uri.path
                log.debug('upnp_resource', 'serving %r', f)
                child = static.File(f)
        return child

    def ip_port__set(self, value):
        self._ip_port = value
        self._upnp_ms.enable_coherence(*value)

    def ip_port__get(self):
        return self._ip_port

class DeferredResource(resource.Resource):

    def __init__(self, dfr):
        resource.Resource.__init__(self)
        self._dfr = dfr
        
    def render(self, request):    
        return server.NOT_DONE_YET

class CoverResource(resource.Resource):
    """
    DOCME
    """

    addSlash = True

    def render(self, request):
        return """
        <html>
        <head>
        <title>Elisa covers</title>
        </head>
        <body>
        <h1>Get lost :)</h1>
        </body>
        </html>
        """

    def getChild(self, path, request):
        child = self
        params = request.args
        artist = params.get('artist',[])
        album = params.get('album',[])

        if artist:
            artist = artist[0]

        if album:
            album = album[0]

        def got_metadata(metadata):
            data = ""
            if 'cover' in metadata and metadata['cover'] is not None:
                uri = metadata['cover']
                f = open(uri.host + uri.path)
                data = f.read()
                f.close()
                
            request.write(data)
            request.finish()

        if artist and album:
            metadata_manager = common.application.metadata_manager
            dfr = metadata_manager.get_metadata({'artist': artist,
                                                 'album': album,
                                                 'cover': None})
            dfr.addCallback(got_metadata)
            child = DeferredResource(dfr)
            
        return child


ALL=0
FOLDER=1

class UPnPMediaServer(pb.Referenceable, service_provider.ServiceProvider):
    """
    Provides UPnP clients access to UPnP resources
    """

    # TODO: should this be translated, too?
    tree = { '0': ('Elisa', {
        'A' : ('Music',
              {'A': ('All', {'content': (ALL, 'audio', None)}),
               'B': ('By artist',
                     {'content': media_uri.MediaUri('elisa://localhost/artists/')}),
               'C': ('By album',
                     {'content': media_uri.MediaUri('elisa://localhost/albums/')}),
               'D': ('By folder', {'content': (FOLDER, 'audio', None)})
               }),
        'B': ('Videos',
              {'A': ('All', {'content': (ALL, 'video', None)}),
               'B': ('By folder', {'content': (FOLDER, 'video', None)}),
               }),
        'C': ('Pictures',
              {'A': ('All', {'content': (ALL, 'image', None)}),
               'B': ('By folder', {'content': (FOLDER, 'image', None)}),
               }),
        })
             }

    def __init__(self):
        service_provider.ServiceProvider.__init__(self)
        self.container_id = 1
        self.http_upnp_path = "data/upnp"
        self._upnp_resource = UPnPResource(self)
        self._media_locations = {'audio':[], 'video':[], 'image':[]}
        self._lock = threading.Lock()
        self._locations_observable = None
        self._locations_observer = LocationsObserver(self)
        
    def initialize(self):
        common.application.bus.register(self._bus_message_received,
                                        (ComponentsLoadedMessage,
                                        LocationsListMessage))

    def _bus_message_received(self, msg, sender):
        if isinstance(msg, ComponentsLoadedMessage):
            # register the cover HTTP resource to the HTTP server
            msg = HTTPResourceMessage("data/covers", CoverResource())
            common.application.bus.send_message(msg)

            # register the UPnPResource to the HTTP server, via the Message bus
            msg = HTTPResourceMessage(self.http_upnp_path, self._upnp_resource)
            common.application.bus.send_message(msg)

            # register myself to the PB service
            msg = PBReferenceableMessage('get_cache_manager', self)
            common.application.bus.send_message(msg)
        else:
            self._locations_observable = msg.locations
            for location in self._locations_observable:
                self.add_media_location(location)
            self._locations_observable.add_observer(self._locations_observer)

    def clean(self):
        if self._locations_observable:
            self._locations_observable.remove_observer(self._locations_observer)
        service_provider.ServiceProvider.clean(self)

    def add_media_location(self, location):
        # TODO: add support in the http server to serve daap and other
        #       URI schemes we correctly handle elsewhere in Elisa
        if location.uri.scheme not in ('elisa', 'file'):
            self.debug("Unsupported URI scheme for %r media location",
                       location.uri)
        else:
            self.info("Adding media location: %r with media_types %r",
                      location.uri, location.media_types)
            for media_type in location.media_types:
                self._media_locations[media_type].append(location.uri)

    def remove_media_location(self, location):
        self.info("Removing media location %r with media_types %r",
                  location.uri, location.media_types)
        for media_type in location.media_types:
            if location.uri in self._media_locations[media_type]:
                self._media_locations[media_type].remove(location.uri)
                
    def enable_coherence(self, host, port):
        # add a new plugin in Coherence
        # 'version': 2
        args = {'name': 'Elisa medias',
                'host': host, }
        msg = CoherencePluginMessage('ElisaMediaStore', args)
        common.application.bus.send_message(msg)

    def _next_id(self):
        try:
            self._lock.acquire()
            self.container_id += 1
        finally:
            self._lock.release()
            return self.container_id

    @defer.deferredGenerator
    def _got_children(self, children, parent):
        self.debug("Got %r", children)
        media_manager = common.application.media_manager
        db = MediaDB.get_connection()
        nodes = []

        def got_result(is_directory, child_uri):
            item = {}
            self.debug("%r is a directory: %r", child_uri, is_directory)
            if is_directory:
                container_id = "%s-%s" % (parent, self._next_id())
                container_name = child_uri.label
                size = 1
                item = {'id': container_id,
                        'parent_id': parent,
                        'name': container_name,
                        'mimetype': 'directory',
                        'size': size,
                        'children': [],
                        'location': {},
                        'content': child_uri
                        }
            else:
                node = db.get_media_information(child_uri, extended=True)
                if node:
                    item = self._node_to_dict(node, parent)
                    item['content'] = child_uri
                else:
                    self.debug("%r not found in DB. Can't serve it", child_uri)
            return item
        
        if db:
            for child in children:
                d = media_manager.is_directory(child[0])
                d.addCallback(got_result, child[0])
                wfd = defer.waitForDeferred(d)
                yield wfd
                node = wfd.getResult()
                if node:
                    nodes.append(node)

        yield nodes

    def _db_filter(self, kind, media_type, parent):
        nodes = []
        if kind == ALL:
            db = MediaDB.get_connection()
            if db:
                for media_node in db.get_medias(media_type=media_type):
                    node_dict = self._node_to_dict(media_node, parent)
                    node_dict['content'] = media_uri.MediaUri(media_node.uri)
                    nodes.append(node_dict)
        elif kind == FOLDER:
            locations = self._media_locations[media_type]
            for location in locations:
                container_id = "%s-%s" % (parent, self._next_id())
                container_name = location.label
                size = 1
                item = {'id': container_id,
                        'parent_id': parent,
                        'name': container_name,
                        'mimetype': 'directory',
                        'size': size,
                        'children': [],
                        'location': {},
                        'content': (FOLDER, media_type, location)
                        }
                nodes.append(item)
        return nodes

    def _get_containers(self, tree, parent=None):
        media_manager = common.application.media_manager

        def fill_cache(nodes):
            for node in nodes:
                uri = node['content']
                del node['content']
                path = node['id'].split('-')
                tree[path[-1]] = (node['name'],{'content': uri})
            return nodes

        if 'content' in tree:

            if isinstance(tree['content'], media_uri.MediaUri):
                uri = tree['content']
                children = []
                self.debug("Retrieving children of %r", uri)
                dfr = media_manager.get_direct_children(uri,
                                                        children)
                dfr.addCallback(self._got_children, parent)
                dfr.addCallback(fill_cache)
            else:
                kind, media_type, uri = tree['content']
                if uri:
                    children = []
                    dfr = media_manager.get_direct_children(uri,
                                                            children)
                    dfr.addCallback(self._got_children, parent)
                    dfr.addCallback(fill_cache)
                else:
                    nodes = self._db_filter(kind, media_type, parent)
                    nodes = fill_cache(nodes)
                    dfr = defer.Deferred()
                    dfr.callback(nodes)

        else:
            containers = []
            for container_id, container in tree.iteritems():
                sub_tree = container[1]
                container_name = container[0]
                size = len(sub_tree.keys())
                container_id = "%s-%s" % (parent, container_id)
                node_dict = {'id': container_id,
                             'parent_id': parent,
                             'name': container_name,
                             'mimetype': 'directory',
                             'size': size,
                             'children': [],
                             'location': {}
                             }
                containers.append(node_dict)

            dfr = defer.Deferred()
            dfr.callback(containers)

        return dfr

    def _node_to_dict(self, node, parent_id):
        # try to guess the uri mime-type
        node_dict = {}
        if node:
            mimetype, sub_type = mimetypes.guess_type(node.uri)
            if not mimetype:
                # fallback to incomplete mime-type based on node.format
                mimetype = '%s/' % node.format

            ip, port = self._upnp_resource.ip_port
            external_location = 'http://%s:%s/%s/%s' % (ip, port,
                                                        self.http_upnp_path,
                                                        node.id)
            self.debug(external_location)

            cover_uri = None
            if 'artist' in node.keys() and 'album' in node.keys():
                artist = media_uri.quote(node['artist'])
                album = media_uri.quote(node['album'])
                cover_uri = 'http://%s:%s/%s/cover.jpg?artist=%s&album=%s'
                cover_uri = cover_uri % (ip, port, "data/covers",
                                         artist, album)
                self.debug(cover_uri)
                
            internal_location = node.uri
            node_dict = {'id': "%s-%s" % (parent_id,self._next_id()),
                         'parent_id': parent_id,
                         'name': node.short_name,
                         'mimetype': mimetype,
                         'size': 0,
                         'cover': cover_uri,
                         'children': [],
                         'location': {'internal': internal_location,
                                      'external': external_location}
                         }
        return node_dict

    def remote_get_media_root_id(self, media_type):
        db = MediaDB.get_connection()
        #db = common.application.media_manager.media_db
        if db:
            medias = db.get_medias()
            for media in medias:
                if media.typ == media_type:
                    return media.source_id
        return 0

    def remote_get_media_node_with_id(self, container_id):
        """
        Returns a dict with following keys:
        id = id in the media db
        parent_id = parent_id in the media db
        name = title, album name or basename
        mimetype = 'directory' or real mimetype
        size = in bytes (??)
        children = list of objects for which this item is the parent
        location = filesystem path if item is a file
        """
        result = {}

        path = container_id.split('-')
        if len(path) > 1:
            parent_id = '-'.join(path[:-1])
        else:
            parent_id = '0'

        self.debug("Looking for container %r with parent %r", container_id,
                   parent_id)
        i = 0
        c = None
        tree = self.tree
        container_name = ''

        for c_id in path:
            c = tree[c_id][1]
            container_name = tree[c_id][0]
            tree = c

        self.debug("Looking in container: %r", tree)

        def got_containers(child_list):
            size = len(child_list)
            container = {'id': container_id,
                         'parent_id': parent_id,
                         'name': container_name,
                         'mimetype': 'directory',
                         'size': size,
                         'children': child_list,
                         'location': {}
                         }
            return container

        if c:
            result = self._get_containers(c, container_id)
            result.addCallback(got_containers)
        return result
