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

"""
DAAP MediaProvider component
"""


__maintainer__ = 'Benjamin Kampmann <benjamin@fluendo.com>'
__maintainer2__ = 'Philippe Normand <philippe@fluendo.com>'
__maintainer3__ = 'Alessandro Decina <alessandro@fluendo.com>'

from elisa.base_components.media_provider import MediaProvider
from elisa.core import media_uri
from elisa.core import common
from elisa.core.log import Loggable
from elisa.core.component import InitializeFailure
from elisa.core.media_uri import MediaUri
from elisa.core.observers.dict import DictObservable
from elisa.extern.natural_sort import natcasecmp
from elisa.extern.odict import SequenceOrderedDict

from twisted.internet import task

import socket
import gst
import gobject

import daap

try:
    import dbus
    if getattr(dbus, 'version', (0,0,0)) >= (0,41,0):
        import dbus.glib
except ImportError:
    dbus = None

if dbus:
    try:
        import avahi
    except ImportError:
        avahi = None
else:
    avahi = None

from twisted.internet import defer, threads

"""
TODO:

- add a "copy to local library" feature?
"""

plugin_registry = common.application.plugin_registry
LocalNetworkLocationMessage = plugin_registry.get_component_class('base:local_network_location_message')

class DaapSource(gst.BaseSrc, gst.URIHandler):
    __gsttemplates__ = (
        gst.PadTemplate("src",
                        gst.PAD_SRC,
                        gst.PAD_ALWAYS,
                        gst.caps_new_any()),
        )

    __gstdetails__ = ("DAAP plugin", "Foo/Bar", "Read data on DAAP shares",
                      "Philippe Normand <philippe@fluendo.com>, "
                      "Alessandro Decina <alessandro@fluendo.com>")

    blocksize = 4096

    def __init__(self):
        super(DaapSource, self).__init__()
        self.curoffset = 0
        self._libs = {}
        self.client = None
        self.session = None
        self.response = None
        self.track = None
        self.data = None

    @classmethod
    def do_get_type_full(cls):
        return gst.URI_SRC

    @classmethod
    def do_get_protocols_full(cls):
        return ["daap"]

    def do_set_uri(self, uri):
        if not uri.startswith('daap://'):
            return False

        self.uri = uri
        return True

    def do_get_uri(self):
        return self.uri

    # set_uri and get_uri are just syntactic sugar for do_set_uri and
    # do_get_uri
    def set_uri(self, uri):
        return self.do_set_uri(uri)

    def get_uri(self):
        return self.do_get_uri()

    def do_start(self):
        m_uri = media_uri.MediaUri(self.uri)
        host = m_uri.host
        port = int(m_uri.port or  '3689')
        
        self.client = daap.DAAPClient()
        self.client.connect(host, port)
        self.session = self.client.login()
        library = self.session.library()
        track_id = int(m_uri.get_param('id','1'))
        self.track = None
        for track in library.tracks():
            if track.id == track_id:
                self.track = track
                break

        if track is None:
            return False

        return True

    def do_stop(self):
        self.session.logout()
        self.client.socket.close()
        self.client = None
        self.session = None
        self.track = None
        self.data = None

        return True

    def do_check_get_range(self):
        return False

    def do_create(self, offset, length):
        if self.data is None:
            self.data = self.track.request()
        data = self.data.read(self.blocksize)
        if data:
            return gst.FLOW_OK, gst.Buffer(data)
        else:
            self.data = None
            return gst.FLOW_UNEXPECTED, None

# define this here so we can use it in the unittest without having to create the
# media provider
def register_daap_source (name):
    if gst.gst_version < (0, 10, 13):
        raise InitializeFailure(name,
            "The installed gst version doesn't support python plugins, "
            "daap support will not be available.")

    gobject.type_register(DaapSource)
    if gst.pygst_version <= (0, 10, 8):
        # use our workaround
        try:
            import _daap_uri_interface
        except ImportError:
            raise InitializeFailure(name,
                "The installed pygst version doesn't support the "
                "GstURIHandler interface and the custom GstURIInterface "
                "wrapper has not been built")
        
        # only register the first time (ComponentTestCase creates the daap
        # provider multiple times, so we have to check this)
        if gst.URIHandler.__gtype__ not in DaapSource.__gtype__.interfaces:
            _daap_uri_interface.bind_to(DaapSource)

    gst.element_register(type=DaapSource, elementname="daapsrc",
        rank=gst.RANK_PRIMARY)

class DaapAvahiMonitor(Loggable):

    def __init__(self):
        self._callbacks = {'new-service':  [],
                           'remove-service': []
                           }
        self.bus = dbus.SystemBus()
        avahi_bus = self.bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER)
        self.server = dbus.Interface(avahi_bus, avahi.DBUS_INTERFACE_SERVER)

        stype = '_daap._tcp'
        domain = 'local'
        self._plugged = {}
        avahi_browser = self.server.ServiceBrowserNew(avahi.IF_UNSPEC,
                                                      avahi.PROTO_UNSPEC,
                                                      stype, domain,
                                                      dbus.UInt32(0))
        obj = self.bus.get_object(avahi.DBUS_NAME, avahi_browser)
        self.browser = dbus.Interface(obj, avahi.DBUS_INTERFACE_SERVICE_BROWSER)

    def start(self):
        self.browser.connect_to_signal('ItemNew', self.new_service)
        self.browser.connect_to_signal('ItemRemove', self.remove_service)

    def stop(self):
        self.bus.close()

    def new_service(self, interface, protocol, name, type, domain, flags):
    
        def resolve_service_reply(*service):
            address, port = service[-4:-2]
            name = unicode(service[2])
            for cb in self._callbacks['new-service']:
                self._plugged[name] = (address,port)
                cb(name, address, port)
       
        def resolve_service_error(exception):
            self.warning('could not resolve daap service %s %s: %s' %
                (name, domain, exception))

        self.server.ResolveService(interface, protocol, name, type, domain,
                avahi.PROTO_UNSPEC, dbus.UInt32(0),
                reply_handler=resolve_service_reply,
                error_handler=resolve_service_error)

    def remove_service(self, interface, protocol, name, type, domain,server):
        address, port = self._plugged[name]
        for cb in self._callbacks['remove-service']:
            cb(name, address, port)


    def add_callback(self, sig_name, callback):
        self._callbacks[sig_name].append(callback)

    def remove_callback(self, sig_name, callback):
        self._callback[sig_name].remove(callback)

class DaapArtist(SequenceOrderedDict):
    def __init__(self, name):
        self.name = name

        super(DaapArtist, self).__init__()

class DaapAlbum(SequenceOrderedDict):
    def __init__(self, name):
        self.name = name

        super(DaapAlbum, self).__init__()

class DaapTrack(object):
    def __init__(self, track_id, name):
        self.id = track_id
        self.name = name

        super(DaapTrack, self).__init__()

class DaapMedia(MediaProvider):

    def __init__(self):
        MediaProvider.__init__(self)
        self._libraries = {}
        self._monitor = None

    def initialize(self):
        self._initialize_daap_src()
        self._initialize_avahi()

    def _initialize_daap_src(self):
        register_daap_source(self.name)

    def _initialize_avahi(self):
        if not dbus:
            if not avahi:
                self.warning("Please install python-avahi to get Avahi support")
            self.warning("python-dbus is missing...")
            self.warning("Avahi support disabled. DAAP shares won't appear automagically")
        else:
            try:
                self._monitor = DaapAvahiMonitor()
                self._monitor.add_callback('new-service', self._add_location)
                self._monitor.add_callback('remove-service',
                                           self._remove_location)
                self._monitor.start()
            except Exception, ex:
                error_msg = "Couldn't initialize Avahi monitor: %s" % str(ex)
                raise InitializeFailure(self.name, error_msg)

    def _add_location(self, name, address, port):
        self.info("New DAAP share at %s:%s : %s" % (address, port, name))
        uri = 'daap://%s:%s/' % (address, port)
        action_type = LocalNetworkLocationMessage.ActionType.LOCATION_ADDED
        msg = LocalNetworkLocationMessage(action_type, name, 'daap',
                                               uri, ['audio',],
                                               theme_icon='network_share_icon')
        common.application.bus.send_message(msg)

    def _remove_location(self, name, address, port):
        self.info("DAAP share at %s:%s disappeared" % (address, port))
        uri = 'daap://%s:%s/' % (address, port)
        action_type = LocalNetworkLocationMessage.ActionType.LOCATION_REMOVED
        msg = LocalNetworkLocationMessage(action_type, name, 'daap',
                                               uri, ['audio',])
        common.application.bus.send_message(msg)

    def _get_library(self, uri):
        self.debug("Retrieving DAAP library at %r", uri)
        
        host = uri.host
        if uri.port:
            port = int(uri.port)
        else:
            port = 3689

        address = (host, port)

        try:
            # try to return the cached library
            return defer.succeed(self._libraries[address])
        except KeyError:
            pass

        # connect and get the library
        dfr = threads.deferToThread(self._retrieve_library, address)
        dfr.addCallback(self._retrieve_library_done, address)

        return dfr

    def _retrieve_library(self, address):
        connection  = daap.DAAPClient()
        connection.connect(*address)

        # auth is not supported yet
        session = connection.login()
        library = session.library()

        return library.tracks()

    def _retrieve_library_done(self, library, address):
        dfr = self._build_library(library, address)

        return dfr

    def _build_library_iter(self, daap_library, library):
        for track in daap_library:
            if None in (track.artist, track.album):
                yield None
                continue

            if not library.has_key(track.artist):
                library[track.artist] = DaapArtist(track.artist)

            if not library[track.artist].has_key(track.album):
                library[track.artist][track.album] = \
                        DaapAlbum(track.album)

            library[track.artist][track.album][track.id] = \
                    DaapTrack(track.id, track.name)

            yield None

        # FIXME: use a thread until we write our non blocking sorting API
        yield threads.deferToThread(self._sort_library, library)

    def _sort_library(self, library):
        for artist_name, album in library.iteritems():
            for album_name, tracks in album.iteritems():
                tracks.sort(natcasecmp, key=lambda key: tracks[key].name)
            album.sort(natcasecmp)
        library.sort(natcasecmp)

        return library

    def _build_library(self, daap_library, address):
        def build_library_done(iterator, address, library):
            self._libraries[address] = library

            return library

        library = SequenceOrderedDict()
        dfr = task.coiterate(self._build_library_iter(daap_library, library))
        dfr.addCallback(build_library_done, address, library)

        return dfr

    def scannable_uri_schemes__get(self):
        return []

    def supported_uri_schemes__get(self):
        return {'daap' : 0}

    def get_media_type(self, uri):
        if not uri.get_param('id'):
            media_type = {'file_type': 'directory', 'mime_type': ''}
        else:
            # DAAP only supports audio media, see DMAP for picture media
            media_type = {'file_type': 'audio', 'mime_type': ''}
        
        return defer.succeed(media_type)

    def is_directory(self, uri):
        return defer.succeed(not uri.get_param('id'))
    
    def has_children_with_types(self, uri, media_types):
        if uri.get_param('id'):
            # a track
            return defer.succeed(False)

        return defer.succeed(bool(set(['audio', 'directory']).intersection(media_types)))

    def get_direct_children(self, uri, children):
        self.debug("Retrieving children of: %s", uri)
        
        artist = uri.get_param('artist', None)
        album = uri.get_param('album', None)
        track_id = uri.get_param('track', None)
        self.debug("URI artist and album are: %r - %r" % (artist,album))

        if track_id:
            # single track, no children
            return defer.succeed(children)
    
        dfr = self._get_library(uri)
        dfr.addCallback(self._get_library_done, children,
            uri, artist, album, track_id)

        return dfr

    def _iter_album_tracks(self, uri, library, children, artist, album):
        for track_id, track in library[artist][album].iteritems():
            track = track.name
            metadata = DictObservable()
            child_uri = media_uri.MediaUri(uri.parent)
            child_dict = {'id': track_id, 'artist': artist,
                          'album': album, 'track': track}
            child_uri.set_params(child_dict)
            child_uri.label = track
            # FIXME: don't set artist and album so that the album cover
            # isn't shown for each track
            metadata['artist'] = artist
            metadata['album'] = album
            metadata['song'] = track
            self.debug("Appending %r with metadata %s" %
                    (child_uri, metadata))
            children.append((child_uri, metadata))
            
            yield None

    def _iter_artist_albums(self, uri, library, children, artist):
        # list albums of given artist
        for album_name, album in library[artist].iteritems():
            metadata = DictObservable()
            child_uri = media_uri.MediaUri(uri.parent)
            metadata['artist'] = artist
            metadata['album'] = album_name
            ## Don't use a DictObservable for params!
            child_uri.set_params({'artist' : artist,
                                  'album' : album_name})
            child_uri.label = album_name
            self.debug("Appendind %r with metadata %s" %
                    (child_uri, metadata))

            children.append((child_uri, metadata))
            
            yield None

    def _iter_artists(self, uri, library, children):
        # list artists
        for artist in library.iterkeys():
            metadata = DictObservable()
            child_uri = media_uri.MediaUri(uri.parent)
            child_uri.set_params({'artist': artist})
            child_uri.label = artist
            metadata['artist'] = artist
            self.debug("Appendind %r with metadata %s" % (child_uri,
                                                        metadata))
            children.append((child_uri,metadata))

            yield None

    def _get_library_done(self, library, children, 
            uri, artist, album, track_id):

        if artist and album:
            # process songs of given album of the artist
            dfr = task.coiterate(self._iter_album_tracks(uri, library,
                    children, artist, album))
        elif artist:
            dfr = task.coiterate(self._iter_artist_albums(uri, library,
                    children, artist))
        else:
            dfr = task.coiterate(self._iter_artists(uri, library, children))

        def iterator_done(iterator, children):
            return children

        dfr.addCallback(iterator_done, children)
        return dfr
