# -*- 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.
#
# Author: Benjamin Kampmann <benjamin@fluendo.com>
#
# inspired by the example code of avahi.org:
#   http://avahi.org/browser/trunk/avahi-python/avahi-discover/avahi-discover.in
#

"""
Discover and Access the Services of your local network
"""

from elisa.core.components.resource_provider import ResourceProvider
from elisa.core.media_uri import MediaUri

from elisa.plugins.avahi.models import NetworkOverviewModel, DomainModel

from elisa.plugins.base.models.network import NetworkServiceModel

from twisted.internet.defer import succeed, fail

import dbus, avahi

try:
    import dbus.glib
except ImportError:
    pass

class DomainNotFound(Exception):
    pass

class AvahiResourceProvider(ResourceProvider):

    # all services that elisa knows about, see http://www.dns-sd.org/ServiceTypes.html
    known_services = { u'_daap._tcp'      : 'daap', # DAAP shares
                       u'_smb._tcp'       : 'smb', # smb shares
                       u'_tftp._tcp'      : 'tftp', # TFTP shares
                       u'_ftp._tcp'       : 'ftp', # ftp shares
                       u'_sftp-ssh._tcp'  : 'sftp', # SFTP shares
                       u'_nfs._tcp'       : 'nfs', # NFS shares
#                       u'_webdav._tcp'    : 'webdav', # webdav shares
#                       u'_webdavs._tcp'   : 'webdavs', # secure webdav
#                       u'_http._tcp'      : 'ftp', # http server
#                       u'_https._tcp'     : 'ftp', # https server
#                       u'_ifolder._tcp'   : 'ifolder', # published iFolder
#                       u'_ipp._tcp'       : 'ipp', # IPP printer
#                       u'_ishare._tcp'    : 'ishare' , #iShare
#                       u'_lan2p._tcp'     : 'lan2p', # LAN2P peer-to-peer protocol
#                       u'_presence._tcp'  : 'presence', # presence peer to peer IM
#                       u'_printer._tcp'   : 'lpr', # online printer
#                       u'_ptp._tcp'       : 'ptp', # picture transfer protocol
#                       u'_shoutcast._tcp' : 'shoutcast', # shoutcast server
#                       u'_upnp._tcp'      : 'upnp', # upnp discovery
                     }

    supported_uri = '^network://([a-zA-Z\.]+)?$|lan://$'

    def __init__(self):
        super(AvahiResourceProvider, self).__init__()

        # the lan overview
        self._network = NetworkOverviewModel()

        # all the domains avahi found
        self._domains = {} # unicode: DomainModel

        # all the services avahi found yet
        self._service_types = {}

        # services
        self._services = {}

    def initialize(self):
        dfr = super(AvahiResourceProvider, self).initialize()
        
        # set up the avahi connection over dbus
        self._bus = dbus.SystemBus()
        self._server =  dbus.Interface(self._bus.get_object(avahi.DBUS_NAME,
                avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER)

        # set up the domain browser
        self._domain_browser = dbus.Interface(self._bus.get_object(
                avahi.DBUS_NAME, self._server.DomainBrowserNew(
                avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, "",
                avahi.DOMAIN_BROWSER_BROWSE, dbus.UInt32(0))),
                avahi.DBUS_INTERFACE_DOMAIN_BROWSER)

        # connect domain callbacks:
        self._domain_browser.connect_to_signal('ItemNew', self._browse_domain)

        # setup the browsing for local directly
        self._browse_domain(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, "local")

        return dfr

    def get(self, uri, context_model):
        """
        Request network informations. Using this method you can browse the
        networks your system is in using Avahi. The URI you probably want to
        start with is the C{network://} what returns you a
        L{elisa.plugins.avahi.models.NetworkOverViewModel}. This contains a list
        of domains found on the network. You now can request one of the domains
        putting the name of it as the host: C{network://<domain_name>}. For e.g.
        C{network://mylocaldomain}. You will receive a
        L{elisa.plugins.avahi.models.DomainModel} containing
        L{elisa.plugins.base.models.network.NetworkServiceModel}s.

        These Service models are added and removed on the fly if differences are
        discovered. This is possible because you always receive the same
        DomainModel instance for each request. The C{context_model} is not taken
        into account at all.

        A shortcut you might be interested in is the C{lan://} uri. This is
        the same as a C{network://local} and that is the only domain that is
        always automatically discovered (as it is a bit special).

        If you request for any other kind of domain, you might get a failure
        wiht L{DomainNotFound}.
        """
        if uri.scheme == 'network' and uri.host == '':
                return self._network, succeed(self._network)

        if uri.scheme == 'lan' or uri.host == 'local':
            key = 'local'
        else:
            key = uri.host

        if not key in self._domains:
            return None, fail(DomainNotFound(key))

        model = self._domains[key]
        return model, succeed(model)

    def _new_server_type(self, interface, protocol, stype, domain, flags):

        if stype in self._service_types[domain]:
            # we are already have a browser for that type
            return

        self.debug("New type %s for %s " % (stype, domain))

        browser = dbus.Interface(self._bus.get_object(avahi.DBUS_NAME,
                self._server.ServiceBrowserNew(interface, protocol, stype,
                domain, dbus.UInt32(0))), avahi.DBUS_INTERFACE_SERVICE_BROWSER)

        self._service_types[domain].append(browser)
        browser.connect_to_signal('ItemNew', self._new_service)
        browser.connect_to_signal('ItemRemove', self._remove_service)

    def _new_service(self, interface, protocol, name, stype, domain, *flags):

        key = (interface, protocol, name, stype, domain)
        if key in self._services:
            # already registered
            return

        self.debug("New Service %s at %r" % (name, key))

        u_stype = unicode(stype)
        if not u_stype in self.known_services:
            return

        service_type = self.known_services[u_stype]

        domain_model = self._domains[domain]
        service = NetworkServiceModel()
        service.name = name
        service.type = service_type

        def resolve_service_reply(interface, protocol, name, stype, domain, \
                host, aprotocol, address, port, txt, flags):

            uri = MediaUri('%s://%s:%s' % (service_type, address, port))
            # FIXME: add more as soon as known

            self.info("Service %s resolved: %s , %s" % (name, uri, host))
            service.elisa_uri = uri
            service.host = host

        def error_received(error):
            self.warning("Resolving of %s failed: %s" % (name, error))
            self._remove_service(interface, protocol, name, stype, domain,
                    flags)
       
        self._server.ResolveService(interface, protocol, name, stype, domain,
                avahi.PROTO_UNSPEC, dbus.UInt32(0),
                reply_handler=resolve_service_reply,
                error_handler=error_received)

        self.info("Adding Service %s %r on Domain %s" % (name, service, domain))
        self._services[key] = service

        domain_model.services.append(service)

    def _remove_service(self, interface, protocol, name, stype, domain, flags):
        if unicode(stype) not in self.known_services:
            return

        key = (interface, protocol, name, stype, domain)

        self.debug("Removing Service %s at %r" % (name, key))

        # remove it from the internal cache
        if key in self._services:
            source = self._services.pop(key)

            # remove it from the corresponding domain
            domain_model = self._domains[domain]
            domain_model.services.remove(source)

    def _browse_domain(self, interface, protocol, domain, *flags):

        unicode_domain = unicode(domain)

        if unicode_domain in self._domains:
            # we are already browsing this one
            return

        self.debug("New Domain %s found. Browse it" % domain)

        type_browser = dbus.Interface(self._bus.get_object(avahi.DBUS_NAME,
                self._server.ServiceTypeBrowserNew(interface, protocol,
                domain, dbus.UInt32(0))),
                avahi.DBUS_INTERFACE_SERVICE_TYPE_BROWSER)

        # set up the informations holder
        self._service_types[unicode_domain] = []

        domain_model = DomainModel()
        domain_model.name = unicode_domain

        self._domains[unicode_domain] = domain_model

        self._network.domains.append(unicode_domain)

        type_browser.connect_to_signal('ItemNew', self._new_server_type)

if __name__ == '__main__':

    from twisted.internet import glib2reactor
    glib2reactor.install()

    from twisted.internet import reactor

    res = None
    debug = 5

    def setup_done(resource):
        global res
        res = resource

        print """

        You are now in the interactive tests. You can access the resource
        provider using the variable 'res':
        >>print res
        %r

        """ % res

    def set_up():

        def printer(self, msg):
            print msg

        global debug
        debugs = ['error', 'warn', 'info', 'debug', 'log']

        for i in xrange(debug):
            attr = debugs[i]
            print "rewrite", attr
            setattr(AvahiResourceProvider, attr, printer)

        AvahiResourceProvider.create({}).addCallback(setup_done)

    reactor.callWhenRunning(set_up)
    reactor.simulate()
    reactor.startRunning()
    import IPython
    IPython.Shell.IPShellGTK([], locals()).mainloop()
