# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php

# Copyright (C) 2006 Fluendo, S.A. (www.fluendo.com).
# Copyright 2006, Frank Scholz <coherence@beebits.net>

import os

import time
import urllib2
from coherence.upnp.core import action
from coherence.upnp.core import event
from coherence.upnp.core import variable

from coherence.upnp.core import utils
from coherence.upnp.core.soap_proxy import SOAPProxy
from coherence.upnp.core.soap_service import errorCode
from coherence.upnp.core.event import EventSubscriptionServer

from coherence.extern.et import ET

from twisted.web import static
from twisted.internet import defer, reactor
from twisted.python import failure, util
from twisted.internet import task

import coherence.extern.louie as louie

from coherence import log

global subscribers
subscribers = {}

def subscribe(service):
    subscribers[service.get_sid()] = service

def unsubscribe(service):
    if subscribers.has_key(service.get_sid()):
        del subscribers[service.get_sid()]

class Service(log.Loggable):
    logCategory = 'service_client'

    def __init__(self, service_type, service_id, location, control_url,
                 event_sub_url, presentation_url, scpd_url, device):
        #if not control_url.startswith('/'):
        #    control_url = "/%s" % control_url
        #if not event_sub_url.startswith('/'):
        #    event_sub_url = "/%s" % event_sub_url
        #if presentation_url and not presentation_url.startswith('/'):
        #    presentation_url = "/%s" % presentation_url
        #if not scpd_url.startswith('/'):
        #    scpd_url = "/%s" % scpd_url

        self.service_type = service_type
        self.detection_completed = False
        self.id = service_id
        self.control_url = control_url
        self.event_sub_url = event_sub_url
        self.presentation_url = presentation_url
        self.scpd_url = scpd_url
        self.device = device
        self._actions = {}
        self._variables = { 0: {}}
        self._var_subscribers = {}
        self.subscription_id = None
        self.timeout = 0

        self.event_connection = None
        self.last_time_updated = None

        self.client = None

        parsed = urllib2.urlparse.urlparse(location)
        self.url_base = "%s://%s" % (parsed[0], parsed[1])

        self.parse_actions()
        self.info("%s %s %s initialized" % (self.device.friendly_name,self.service_type,self.id))

    def as_tuples(self):
        r = []

        def append(name,attribute):
            try:
                if isinstance(attribute,tuple):
                    if callable(attribute[0]):
                        v1 = attribute[0]()
                    elif hasattr(self,attribute[0]):
                        v1 = getattr(self,attribute[0])
                    else:
                        v1 = attribute[0]
                    if v1 in [None,'None']:
                        return
                    if callable(attribute[1]):
                        v2 = attribute[1]()
                    elif hasattr(self,attribute[1]):
                        v2 = getattr(self,attribute[1])
                    else:
                        v2 = attribute[1]
                    if v2 in [None,'None']:
                        return
                    if len(attribute)> 2:
                        r.append((name,(v1,v2,attribute[2])))
                    else:
                        r.append((name,(v1,v2)))
                    return
                elif callable(attribute):
                    v = attribute()
                elif hasattr(self,attribute):
                    v = getattr(self,attribute)
                else:
                    v = attribute
                if v not in [None,'None']:
                    r.append((name,v))
            except:
                import traceback
                self.debug(traceback.format_exc())

        r.append(('Location',(self.device.get_location(),self.device.get_location())))
        append('URL base',self.device.get_urlbase)
        r.append(('UDN',self.device.get_id()))
        r.append(('Type',self.service_type))
        r.append(('ID',self.id))
        append('Service Description URL',(self.scpd_url,lambda: self.device.make_fullyqualified(self.scpd_url)))
        append('Control URL',(self.control_url,lambda: self.device.make_fullyqualified(self.control_url),False))
        append('Event Subscription URL',(self.event_sub_url,lambda: self.device.make_fullyqualified(self.event_sub_url),False))

        return r

    def as_dict(self):
        d = {'type':self.service_type}
        d['actions'] = [a.as_dict() for a in self._actions.values()]
        return d

    def __repr__(self):
        return "Service %s %s" % (self.service_type,self.id)

    #def __del__(self):
    #    print "Service deleted"
    #    pass

    def _get_client(self, name):
        url = self.get_control_url()
        namespace = self.get_type()
        action = "%s#%s" % (namespace, name)
        client = SOAPProxy( url, namespace=("u",namespace), soapaction=action)
        return client

    def remove(self):
        self.info("removal of ", self.device.friendly_name, self.service_type, self.id)
        try:
            self.renew_subscription_call.cancel()
        except:
            pass
        if self.event_connection != None:
            self.event_connection.teardown()
        if self.subscription_id != None:
            self.unsubscribe()
        for name,action in self._actions.items():
            self.debug("remove", name,action)
            del self._actions[name]
            del action
        for instance,variables in self._variables.items():
            for name, variable in variables.items():
                del variables[name]
                del variable
            if variables.has_key(instance):
                del variables[instance]
            del variables
        del self

    def get_device(self):
        return self.device

    def get_type(self):
        return self.service_type

    def set_timeout(self, timeout):
        self.info("set timout for %s/%s to %d" %(self.device.friendly_name,self.service_type,int(timeout)))
        self.timeout = timeout
        try:
            self.renew_subscription_call.reset(int(self.timeout)-30)
            self.info("reset renew subscription call for %s/%s to %d" % (self.device.friendly_name,self.service_type,int(self.timeout)-30))
        except:
            self.renew_subscription_call = reactor.callLater(int(self.timeout)-30,
                                                                 self.renew_subscription)
            self.info("starting renew subscription call for %s/%s to %d" % (self.device.friendly_name,self.service_type,int(self.timeout)-30))

    def get_timeout(self):
        return self.timeout

    def get_id(self):
        return self.id

    def get_sid(self):
        return self.subscription_id

    def set_sid(self, sid):
        self.info("set subscription id for %s/%s to %s" %(self.device.friendly_name,self.service_type,sid))
        self.subscription_id = sid
        if sid is not None:
            subscribe(self)
            self.debug("add subscription for %s", self.id)

    def get_actions(self):
        return self._actions

    def get_scpdXML(self):
        return self.scpdXML

    def get_action( self, name):
        try:
            return self._actions[name]
        except KeyError:
            return None # not implemented

    def get_state_variables(self, instance):
        return self._variables.get(int(instance))

    def get_state_variable(self, name, instance=0):
        return self._variables.get(int(instance)).get(name)

    def get_control_url(self):
        return self.device.make_fullyqualified(self.control_url)

    def get_event_sub_url(self):
        return self.device.make_fullyqualified(self.event_sub_url)

    def get_presentation_url(self):
        return self.device.make_fullyqualified(self.presentation_url)

    def get_scpd_url(self):
        return self.device.make_fullyqualified(self.scpd_url)

    def get_base_url(self):
        return self.device.make_fullyqualified('.')

    def subscribe(self):
        self.debug("subscribe %s", self.id)
        event.subscribe(self)
        #global subscribers
        #subscribers[self.get_sid()] = self

    def unsubscribe(self):

        def remove_it(r, sid):
            self.debug("remove subscription for %s", self.id)
            unsubscribe(self)
            self.subscription_id = None
            #global subscribers
            #if subscribers.has_key(sid):
            #    del subscribers[sid]

        self.debug("unsubscribe %s", self.id)
        d = event.unsubscribe(self)
        d.addCallback(remove_it, self.get_sid())
        return d

    def subscribe_for_variable(self, var_name, instance=0, callback=None, signal=False):
        variable = self.get_state_variable(var_name)
        if variable:
            if callback != None:
                if signal == True:
                    callback(variable)
                    louie.connect(callback, signal='Coherence.UPnP.StateVariable.%s.changed' % var_name, sender=self)
                else:
                    variable.subscribe(callback)


    def renew_subscription(self):
        self.info("renew_subscription")
        event.subscribe(self)

    def process_event(self,event):
        self.info("process event %r %r" % (self,event))
        for var_name, var_value  in event.items():
            if var_name == 'LastChange':
                self.info("we have a LastChange event")
                self.get_state_variable(var_name, 0).update(var_value)
                if len(var_value) == 0:
                    continue
                tree = utils.parse_xml(var_value, 'utf-8').getroot()
                namespace_uri, tag = tree.tag[1:].split( "}", 1)
                for instance in tree.findall('{%s}InstanceID' % namespace_uri):
                    instance_id = instance.attrib['val']
                    self.info("instance_id %r %r" % (instance,instance_id))
                    for var in instance.getchildren():
                        self.info("var %r" % var)
                        namespace_uri, tag = var.tag[1:].split("}", 1)
                        self.info("%r %r %r" % (namespace_uri, tag,var.attrib['val']))
                        self.get_state_variable(tag, instance_id).update(var.attrib['val'])
                        self.info("updated var %r" % var)
                        if len(var.attrib) > 1:
                            self.info("Extended StateVariable %s - %r", var.tag, var.attrib)
                            if var.attrib.has_key('channel') and var.attrib['channel'] != 'Master':
                                # TODO handle attributes that them selves have multiple instances
                                self.info("Skipping update to %s its not for master channel %s", var.tag, var.attrib)
                                pass
                            else:
                                if not self.get_state_variables(instance_id):
                                    # TODO Create instance ?
                                    self.error("%r update failed (not self.get_state_variables(instance_id)) %r", self, instance_id)
                                elif not self.get_state_variables(instance_id).has_key(tag):
                                    # TODO Create instance StateVariable?
                                    # SONOS stuff
                                    self.error("%r update failed (not self.get_state_variables(instance_id).has_key(tag)) %r", self, tag)
                                else:
                                    val = None
                                    if var.attrib.has_key('val'):
                                        val = var.attrib['val']
                                    #self.debug("%r update %r %r %r", self,namespace_uri, tag, var.attrib['val'])
                                    self.get_state_variable(tag, instance_id).update(var.attrib['val'])
                                    self.debug("updated 'attributed' var %r", var)
                louie.send('Coherence.UPnP.DeviceClient.Service.Event.processed',None,self,(var_name,var_value,event.raw))
            else:
                self.get_state_variable(var_name, 0).update(var_value)
                louie.send('Coherence.UPnP.DeviceClient.Service.Event.processed',None,self,(var_name,var_value,event.raw))
        if self.last_time_updated == None:
            # The clients (e.g. media_server_client) check for last time to detect whether service detection is complete
            # so we need to set it here and now to avoid a potential race condition
            self.last_time_updated = time.time()
            louie.send('Coherence.UPnP.DeviceClient.Service.notified', sender=self.device, service=self)
            self.info("send signal Coherence.UPnP.DeviceClient.Service.notified for %r" % self)
        self.last_time_updated = time.time()

    def parse_actions(self):

        def gotPage(x):
            #print "gotPage"
            #print x
            self.scpdXML, headers = x
            try:
                tree = utils.parse_xml(self.scpdXML, 'utf-8').getroot()
                ns = "urn:schemas-upnp-org:service-1-0"

                for action_node in tree.findall('.//{%s}action' % ns):
                    name = action_node.findtext('{%s}name' % ns)
                    arguments = []
                    for argument in action_node.findall('.//{%s}argument' % ns):
                        arg_name = argument.findtext('{%s}name' % ns)
                        arg_direction = argument.findtext('{%s}direction' % ns)
                        arg_state_var = argument.findtext('{%s}relatedStateVariable' % ns)
                        arguments.append(action.Argument(arg_name, arg_direction,
                                                         arg_state_var))
                    self._actions[name] = action.Action(self, name, 'n/a', arguments)

                for var_node in tree.findall('.//{%s}stateVariable' % ns):
                    send_events = var_node.attrib.get('sendEvents','yes')
                    name = var_node.findtext('{%s}name' % ns)
                    data_type = var_node.findtext('{%s}dataType' % ns)
                    values = []
                    """ we need to ignore this, as there we don't get there our
                        {urn:schemas-beebits-net:service-1-0}X_withVendorDefines
                        attibute there
                    """
                    for allowed in var_node.findall('.//{%s}allowedValue' % ns):
                        values.append(allowed.text)
                    instance = 0
                    self._variables.get(instance)[name] = variable.StateVariable(self, name,
                                                                   'n/a',
                                                                   instance, send_events,
                                                                   data_type, values)
                    """ we need to do this here, as there we don't get there our
                        {urn:schemas-beebits-net:service-1-0}X_withVendorDefines
                        attibute there
                    """
                    self._variables.get(instance)[name].has_vendor_values = True
            except Exception as e:
                self.warning("error parsing service xml, service skipped!", e)

            #print 'service parse:', self, self.device
            self.detection_completed = True
            self.process_event({}) #process_event will set self.last_time_updated which is in return checked by the clients to ensure service detection has completed
            louie.send('Coherence.UPnP.Service.detection_completed', sender=self.device, device=self.device)
            self.info("send signal Coherence.UPnP.Service.detection_completed for %r" % self)

        def gotError(failure, url):
            self.warning('error requesting', url)
            self.info('failure', failure)
            louie.send('Coherence.UPnP.Service.detection_failed', self.device, device=self.device)

        #print 'getPage', self.get_scpd_url()
        utils.getPage(self.get_scpd_url()).addCallbacks(gotPage, gotError, None, None, [self.get_scpd_url()], None)

moderated_variables = \
        {'urn:schemas-upnp-org:service:AVTransport:2':
            ['LastChange'],
         'urn:schemas-upnp-org:service:AVTransport:1':
            ['LastChange'],
         'urn:schemas-upnp-org:service:ContentDirectory:2':
            ['SystemUpdateID', 'ContainerUpdateIDs'],
         'urn:schemas-upnp-org:service:ContentDirectory:1':
            ['SystemUpdateID', 'ContainerUpdateIDs'],
         'urn:schemas-upnp-org:service:RenderingControl:2':
            ['LastChange'],
         'urn:schemas-upnp-org:service:RenderingControl:1':
            ['LastChange'],
         'urn:schemas-upnp-org:service:ScheduledRecording:1':
            ['LastChange'],
        }

class ServiceServer(log.Loggable):
    logCategory = 'service_server'

    def __init__(self, id, version, backend):
        self.id = id
        self.version = version
        self.backend = backend
        if getattr(self, "namespace", None) == None:
            self.namespace = 'schemas-upnp-org'
        if getattr(self, "id_namespace", None) == None:
            self.id_namespace = 'upnp-org'

        self.service_type = 'urn:%s:service:%s:%d' % (self.namespace, id, int(self.version))

        self.scpd_url = 'scpd.xml'
        self.control_url = 'control'
        self.subscription_url = 'subscribe'
        self.event_metadata = ''
        if id == 'AVTransport':
            self.event_metadata = 'urn:schemas-upnp-org:metadata-1-0/AVT/'
        if id == 'RenderingControl':
            self.event_metadata = 'urn:schemas-upnp-org:metadata-1-0/RCS/'
        if id == 'ScheduledRecording':
            self.event_metadata = 'urn:schemas-upnp-org:av:srs-event'

        self._actions = {}
        self._variables = {0: {}}
        self._subscribers = {}

        self._pending_notifications = {}

        self.last_change = None
        self.init_var_and_actions()

        try:
            if 'LastChange' in moderated_variables[self.service_type]:
                self.last_change = self._variables[0]['LastChange']
        except:
            pass

        self.putChild(self.subscription_url, EventSubscriptionServer(self))

        self.check_subscribers_loop = task.LoopingCall(self.check_subscribers)
        self.check_subscribers_loop.start(120.0, now=False)

        self.check_moderated_loop = None
        if moderated_variables.has_key(self.service_type):
            self.check_moderated_loop = task.LoopingCall(self.check_moderated_variables)
            #self.check_moderated_loop.start(5.0, now=False)
            self.check_moderated_loop.start(0.5, now=False)

    def _release(self):
        for p in self._pending_notifications.values():
            p.disconnect()
        self._pending_notifications = {}

    def get_action(self, action_name):
        try:
            return self._actions[action_name]
        except KeyError:
            return None # not implemented

    def get_actions(self):
        return self._actions

    def get_variables(self):
        return self._variables

    def get_subscribers(self):
        return self._subscribers

    def rm_notification(self,result,d):
        del self._pending_notifications[d]

    def new_subscriber(self, subscriber):
        notify = []
        for vdict in self._variables.values():
            notify += [v for v in vdict.values() if v.send_events == True]

        self.info("new_subscriber", subscriber, notify)
        if len(notify) <= 0:
            return

        root = ET.Element('e:propertyset')
        root.attrib['xmlns:e']='urn:schemas-upnp-org:event-1-0'
        evented_variables = 0
        for n in notify:
            e = ET.SubElement( root, 'e:property')
            if n.name == 'LastChange':
                if subscriber['seq'] == 0:
                    text = self.build_last_change_event(n.instance, force=True)
                else:
                    text = self.build_last_change_event(n.instance)
                if text is not None:
                    ET.SubElement( e, n.name).text = text
                    evented_variables += 1
            else:
                ET.SubElement( e, n.name).text = str(n.value)
                evented_variables += 1

        if evented_variables > 0:
            xml = ET.tostring( root, encoding='utf-8')
            d,p = event.send_notification(subscriber, xml)
            self._pending_notifications[d] = p
            d.addBoth(self.rm_notification,d)
        self._subscribers[subscriber['sid']] = subscriber

    def get_id(self):
        return self.id

    def get_type(self):
        return self.service_type

    def create_new_instance(self, instance):
        self._variables[instance] = {}
        for v in self._variables[0].values():
            self._variables[instance][v.name] = variable.StateVariable( v.service,
                                                                        v.name,
                                                                        v.implementation,
                                                                        instance,
                                                                        v.send_events,
                                                                        v.data_type,
                                                                        v.allowed_values)
            self._variables[instance][v.name].has_vendor_values = v.has_vendor_values
            self._variables[instance][v.name].default_value = v.default_value
            #self._variables[instance][v.name].value = v.default_value # FIXME
            self._variables[instance][v.name].old_value = v.old_value
            self._variables[instance][v.name].value = v.value
            self._variables[instance][v.name].dependant_variable = v.dependant_variable

    def remove_instance(self, instance):
        if instance == 0:
            return
        del(self._variables[instance])

    def set_variable(self, instance, variable_name, value, default=False):

        def process_value(result):
            variable.update(result)
            if default == True:
                variable.default_value = variable.value
            if(variable.send_events == True and
                variable.moderated == False and
                len(self._subscribers) > 0):
                xml = self.build_single_notification(instance, variable_name, variable.value)
                for s in self._subscribers.values():
                    d,p = event.send_notification(s, xml)
                    self._pending_notifications[d] = p
                    d.addBoth(self.rm_notification,d)
        try:
            variable = self._variables[int(instance)][variable_name]
            if isinstance( value, defer.Deferred):
                value.addCallback(process_value)
            else:
                process_value(value)
        except:
            pass

    def get_variable(self, variable_name, instance=0):
        try:
            return self._variables[int(instance)][variable_name]
        except:
            return None

    def build_single_notification(self, instance, variable_name, value):
        root = ET.Element('e:propertyset')
        root.attrib['xmlns:e']='urn:schemas-upnp-org:event-1-0'
        e = ET.SubElement( root, 'e:property')
        s = ET.SubElement( e, variable_name).text = str(value)
        return ET.tostring( root, encoding='utf-8')

    def build_last_change_event(self, instance=0, force=False):
        got_one = False
        root = ET.Element('Event')
        root.attrib['xmlns']=self.event_metadata
        for instance, vdict in self._variables.items():
            e = ET.SubElement( root, 'InstanceID')
            e.attrib['val']=str(instance)
            for variable in vdict.values():
                if( variable.name != 'LastChange' and
                    variable.name[0:11] != 'A_ARG_TYPE_' and
                    variable.never_evented == False and
                    (variable.updated == True or force == True)):
                    s = ET.SubElement( e, variable.name)
                    s.attrib['val'] = str(variable.value)
                    variable.updated = False
                    got_one = True
                    if variable.dependant_variable != None:
                        dependants = variable.dependant_variable.get_allowed_values()
                        if dependants != None and len(dependants) > 0:
                            s.attrib['channel']=dependants[0]
        if got_one == True:
            return ET.tostring( root, encoding='utf-8')
        else:
            return None

    def propagate_notification(self, notify):
        #print "propagate_notification", notify
        if len(self._subscribers) <= 0:
            return
        if len(notify) <= 0:
            return

        root = ET.Element('e:propertyset')
        root.attrib['xmlns:e']='urn:schemas-upnp-org:event-1-0'

        if isinstance( notify, variable.StateVariable):
            notify = [notify,]

        evented_variables = 0
        for n in notify:
            e = ET.SubElement( root, 'e:property')
            if n.name == 'LastChange':
                text = self.build_last_change_event(instance=n.instance)
                if text is not None:
                    ET.SubElement( e, n.name).text = text
                    evented_variables += 1
            else:
                s = ET.SubElement( e, n.name).text = str(n.value)
                evented_variables += 1
                if n.dependant_variable != None:
                    dependants = n.dependant_variable.get_allowed_values()
                    if dependants != None and len(dependants) > 0:
                        s.attrib['channel']=dependants[0]

        if evented_variables == 0:
            return
        xml = ET.tostring( root, encoding='utf-8')
        #print "propagate_notification", xml
        for s in self._subscribers.values():
            d,p = event.send_notification(s,xml)
            self._pending_notifications[d] = p
            d.addBoth(self.rm_notification,d)

    def check_subscribers(self):
        for s in self._subscribers.values():
            timeout = 86400
            #print s
            timeout = s['timeout']
            if s['timeout'].endswith("infinite"):
                timeout = time.time() + 1800
            elif s['timeout'].startswith('Second-'):
                timeout = int(timeout[len('Second-'):])
            if time.time() > s['created'] + timeout:
                del s

    def check_moderated_variables(self):
        #print "check_moderated for %s" % self.id
        #print self._subscribers
        if len(self._subscribers) <= 0:
            return
        variables = moderated_variables[self.get_type()]
        notify = []
        for v in variables:
            #print self._variables[0][v].name, self._variables[0][v].updated
            for vdict in self._variables.values():
                if vdict[v].updated == True:
                    vdict[v].updated = False
                    notify.append(vdict[v])
        self.propagate_notification(notify)

    def is_variable_moderated(self, name):
        try:
            variables = moderated_variables[self.get_type()]
            if name in variables:
                return True
        except:
            pass
        return False

    def simulate_notification(self):
        print "simulate_notification for", self.id
        self.set_variable(0, 'CurrentConnectionIDs', '0')

    def get_scpdXML(self):
        if not hasattr(self,'scpdXML') or self.scpdXML == None:
            self.scpdXML = scpdXML(self)
            self.scpdXML = self.scpdXML.build_xml()
        return self.scpdXML

    def register_vendor_variable(self,name,implementation='optional',
                                      instance=0, evented='no',
                                      data_type='string',
                                      dependant_variable=None,
                                      default_value=None,
                                      allowed_values=None,has_vendor_values=False,allowed_value_range=None,
                                      moderated=False):
        """
        enables a backend to add an own, vendor defined, StateVariable to the service

        @ivar name: the name of the new StateVariable
        @ivar implementation: either 'optional' or 'required'
        @ivar instance: the instance number of the service that variable should be assigned to, usually '0'
        @ivar evented: boolean, or the special keyword 'never' if the variable doesn't show up in a LastChange event too
        @ivar data_type: 'string','boolean','bin.base64' or various number formats
        @ivar dependant_variable: the name of another StateVariable that depends on this one
        @ivar default_value: the value this StateVariable should have by default when created
                             for another instance of in the service
        @ivar allowed_values: a C{list} of values this StateVariable can have
        @ivar has_vendor_values: boolean if there are values outside the allowed_values list too
        @ivar allowed_value_range: a C{dict} of 'minimum','maximum' and 'step' values
        @ivar moderated: boolean, True if this StateVariable should only be evented via a LastChange event

        """

        # FIXME
        # we should raise an Exception when there as a StateVariable with that name already

        if evented == 'never':
            send_events = 'no'
        else:
            send_events = evented
        new_variable = variable.StateVariable(self,name,implementation,instance,send_events,
                                                   data_type,allowed_values)
        if default_value == None:
            new_variable.default_value = ''
        else:
            new_variable.default_value = new_variable.old_value = new_variable.value = default_value

        new_variable.dependant_variable = dependant_variable
        new_variable.has_vendor_values = has_vendor_values
        new_variable.allowed_value_range = allowed_value_range
        new_variable.moderated = moderated
        if evented == 'never':
            new_variable.never_evented = True
        self._variables.get(instance)[name] = new_variable
        return new_variable

    def register_vendor_action(self,name,implementation,arguments=None,needs_callback=True):
        """
        enables a backend to add an own, vendor defined, Action to the service

        @ivar name: the name of the new Action
        @ivar implementation: either 'optional' or 'required'
        @ivar arguments: a C{list} if argument C{tuples},
                         like (name,direction,relatedStateVariable)
        @ivar needs_callback: this Action needs a method in the backend or service class
        """

        # FIXME
        # we should raise an Exception when there as an Action with that name already
        # we should raise an Exception when there is no related StateVariable for an Argument

        """ check for action in backend """
        callback = getattr(self.backend, "upnp_%s" % name, None)

        if callback == None:
            """ check for action in ServiceServer """
            callback = getattr(self, "upnp_%s" % name, None)

        if( needs_callback == True and
            callback == None):
            """ we have one or more 'A_ARG_TYPE_' variables
                issue a warning for now
            """
            if implementation == 'optional':
                self.info('%s has a missing callback for %s action %s, action disabled' % (self.id,implementation,name))
                return
            else:
                if((hasattr(self,'implementation') and self.implementation == 'required') or
                    not hasattr(self,'implementation')):
                    self.warning('%s has a missing callback for %s action %s, service disabled' % (self.id,implementation,name))
                raise LookupError("missing callback")

        arguments_list = []
        for argument in arguments:
            arguments_list.append(action.Argument(argument[0],argument[1].lower(),argument[2]))

        new_action = action.Action(self, name, implementation, arguments_list)
        self._actions[name] = new_action
        if callback != None:
            new_action.set_callback(callback)
            self.info('Add callback %s for %s/%s' % (callback, self.id, name))
        return new_action

    def init_var_and_actions(self):
        desc_file = util.sibpath(__file__, os.path.join('xml-service-descriptions', '%s%d.xml' % (self.id, int(self.version))))
        tree = ET.parse(desc_file)

        for action_node in tree.findall('.//action'):
            name = action_node.findtext('name')
            implementation = 'required'
            needs_callback = False
            if action_node.attrib.get('{urn:schemas-beebits-net:service-1-0}X_needs_backend',
                                        None) != None:
                needs_callback = True
            if action_node.find('Optional') != None:
                implementation = 'optional'
                if(action_node.find('Optional').attrib.get(
                                        '{urn:schemas-beebits-net:service-1-0}X_needs_backend',
                                        None) != None or
                    action_node.attrib.get('{urn:schemas-beebits-net:service-1-0}X_needs_backend',
                                            None) != None):
                    needs_callback = True

            arguments = []
            for argument in action_node.findall('.//argument'):
                arg_name = argument.findtext('name')
                arg_direction = argument.findtext('direction')
                arg_state_var = argument.findtext('relatedStateVariable')
                arguments.append(action.Argument(arg_name, arg_direction,
                                                 arg_state_var))
                if( arg_state_var[0:11] == 'A_ARG_TYPE_' and
                    arg_direction == 'out'):
                    needs_callback = True
                #print arg_name, arg_direction, needs_callback

            """ check for action in backend """
            callback = getattr(self.backend, "upnp_%s" % name, None)

            if callback == None:
                """ check for action in ServiceServer """
                callback = getattr(self, "upnp_%s" % name, None)

            if( needs_callback == True and
                callback == None):
                """ we have one or more 'A_ARG_TYPE_' variables
                    issue a warning for now
                """
                if implementation == 'optional':
                    self.info('%s has a missing callback for %s action %s, action disabled' % (self.id,implementation,name))
                    continue
                else:
                    if((hasattr(self,'implementation') and self.implementation == 'required') or
                        not hasattr(self,'implementation')):
                        self.warning('%s has a missing callback for %s action %s, service disabled' % (self.id,implementation,name))
                    raise LookupError,"missing callback"

            new_action = action.Action(self, name, implementation, arguments)
            self._actions[name] = new_action
            if callback != None:
                new_action.set_callback(callback)
                self.info('Add callback %s for %s/%s' % (callback, self.id, name))


        backend_vendor_value_defaults = getattr(self.backend, "vendor_value_defaults", None)
        service_value_defaults = None
        if backend_vendor_value_defaults:
            service_value_defaults = backend_vendor_value_defaults.get(self.id,None)

        backend_vendor_range_defaults = getattr(self.backend, "vendor_range_defaults", None)
        service_range_defaults = None
        if backend_vendor_range_defaults:
            service_range_defaults = backend_vendor_range_defaults.get(self.id)

        for var_node in tree.findall('.//stateVariable'):
            instance = 0
            name = var_node.findtext('name')
            implementation = 'required'
            if action_node.find('Optional') != None:
                implementation = 'optional'

            #if implementation == 'optional':
            #    for action_object in self._actions.values():
            #        if name in [a.get_state_variable() for a in action_object.arguments_list]:
            #            break
            #    else:
            #        continue

            send_events = var_node.findtext('sendEventsAttribute')
            data_type = var_node.findtext('dataType')
            values = []
            for allowed in var_node.findall('.//allowedValue'):
                values.append(allowed.text)
            self._variables.get(instance)[name] = variable.StateVariable(self, name,
                                                           implementation,
                                                           instance, send_events,
                                                           data_type, values)

            dependant_variable = var_node.findtext('{urn:schemas-beebits-net:service-1-0}X_dependantVariable')
            if dependant_variable:
                self._variables.get(instance)[name].dependant_variable = dependant_variable
            default_value = var_node.findtext('defaultValue')
            if default_value:
                self._variables.get(instance)[name].set_default_value(default_value)
            if var_node.find('sendEventsAttribute') != None:
                never_evented = var_node.find('sendEventsAttribute').attrib.get(
                                        '{urn:schemas-beebits-net:service-1-0}X_no_means_never',
                                        None)
                if never_evented is not None:
                    self._variables.get(instance)[name].set_never_evented(never_evented)

            allowed_value_list = var_node.find('allowedValueList')
            if allowed_value_list != None:
                vendor_values = allowed_value_list.attrib.get(
                                    '{urn:schemas-beebits-net:service-1-0}X_withVendorDefines',
                                    None)
                if service_value_defaults:
                    variable_value_defaults = service_value_defaults.get(name, None)
                    if variable_value_defaults:
                        self.info("overwriting %s default value with %s" % (name,
                                                               variable_value_defaults))
                        self._variables.get(instance)[name].set_allowed_values(variable_value_defaults)

                if vendor_values != None:
                    self._variables.get(instance)[name].has_vendor_values = True

            allowed_value_range = var_node.find('allowedValueRange')
            if allowed_value_range:
                vendor_values = allowed_value_range.attrib.get(
                                    '{urn:schemas-beebits-net:service-1-0}X_withVendorDefines',
                                    None)
                range = {}
                for e in list(allowed_value_range):
                    range[e.tag] = e.text
                    if( vendor_values != None):
                        if service_range_defaults:
                            variable_range_defaults = service_range_defaults.get(name)
                            if( variable_range_defaults != None and
                                variable_range_defaults.get(e.tag) != None):
                                self.info("overwriting %s attribute %s with %s" % (name,
                                                               e.tag, str(variable_range_defaults[e.tag])))
                                range[e.tag] = variable_range_defaults[e.tag]
                            elif e.text == None:
                                self.info("missing vendor definition for %s, attribute %s" % (name, e.tag))
                self._variables.get(instance)[name].set_allowed_value_range(**range)
                if vendor_values != None:
                    self._variables.get(instance)[name].has_vendor_values = True
            elif service_range_defaults:
                variable_range_defaults = service_range_defaults.get(name)
                if variable_range_defaults != None:
                    self._variables.get(instance)[name].set_allowed_value_range(**variable_range_defaults)
                    self._variables.get(instance)[name].has_vendor_values = True

        for v in self._variables.get(0).values():
            if isinstance( v.dependant_variable, str):
                v.dependant_variable = self._variables.get(instance).get(v.dependant_variable)

class scpdXML(static.Data):

    def __init__(self, server, control=None):
        self.service_server = server
        self.control = control
        static.Data.__init__(self, None, 'text/xml')

    def render(self, request):
        if self.data == None:
            self.data = self.build_xml()
        return static.Data.render(self,request)

    def build_xml(self):
        root = ET.Element('scpd')
        root.attrib['xmlns']='urn:schemas-upnp-org:service-1-0'
        e = ET.SubElement(root, 'specVersion')
        ET.SubElement( e, 'major').text = '1'
        ET.SubElement( e, 'minor').text = '0'

        e = ET.SubElement( root, 'actionList')
        for action in self.service_server._actions.values():
            s = ET.SubElement( e, 'action')
            ET.SubElement( s, 'name').text = action.get_name()
            al = ET.SubElement( s, 'argumentList')
            for argument in action.get_arguments_list():
                a = ET.SubElement( al, 'argument')
                ET.SubElement( a, 'name').text = argument.get_name()
                ET.SubElement( a, 'direction').text = argument.get_direction()
                ET.SubElement( a, 'relatedStateVariable').text = argument.get_state_variable()

        e = ET.SubElement( root, 'serviceStateTable')
        for var in self.service_server._variables[0].values():
            s = ET.SubElement( e, 'stateVariable')
            if var.send_events == True:
                s.attrib['sendEvents'] = 'yes'
            else:
                s.attrib['sendEvents'] = 'no'
            ET.SubElement( s, 'name').text = var.name
            ET.SubElement( s, 'dataType').text = var.data_type
            if(not var.has_vendor_values and len(var.allowed_values)):
            #if len(var.allowed_values):
                v = ET.SubElement( s, 'allowedValueList')
                for value in var.allowed_values:
                    ET.SubElement( v, 'allowedValue').text = value

            if( var.allowed_value_range != None and
                len(var.allowed_value_range) > 0):
                complete = True
                for name,value in var.allowed_value_range.items():
                    if value == None:
                        complete = False
                if complete == True:
                    avl = ET.SubElement( s, 'allowedValueRange')
                    for name,value in var.allowed_value_range.items():
                         if value != None:
                            ET.SubElement( avl, name).text = str(value)

        return """<?xml version="1.0" encoding="utf-8"?>""" + ET.tostring( root, encoding='utf-8')

from twisted.python.util import OrderedDict

class ServiceControl:

    def get_action_results(self, result, action, instance):
        """ check for out arguments
            if yes: check if there are related ones to StateVariables with
                    non A_ARG_TYPE_ prefix
                    if yes: check if there is a call plugin method for this action
                            if yes: update StateVariable values with call result
                            if no:  get StateVariable values and
                                    add them to result dict
        """
        self.debug('get_action_results', result)
        #print 'get_action_results', action, instance
        r = result
        notify = []
        for argument in action.get_out_arguments():
            #print 'get_state_variable_contents', argument.name
            if argument.name[0:11] != 'A_ARG_TYPE_':
                if action.get_callback() != None:
                    variable = self.variables[instance][argument.get_state_variable()]
                    variable.update(r[argument.name])
                    #print 'update state variable contents', variable.name, variable.value, variable.send_events
                    if(variable.send_events == 'yes' and variable.moderated == False):
                        notify.append(variable)
                else:
                    variable = self.variables[instance][argument.get_state_variable()]
                    #print 'get state variable contents', variable.name, variable.value
                    r[argument.name] = variable.value
                    #print "r", r
            self.service.propagate_notification(notify)
        #r= { '%sResponse'%action.name: r}
        self.info( 'action_results unsorted', action.name, r)
        if len(r) == 0:
            return r
        ordered_result = OrderedDict()
        for argument in action.get_out_arguments():
            ordered_result[argument.name] = r[argument.name]
        self.info( 'action_results sorted', action.name, ordered_result)
        return ordered_result

    def soap__generic(self, *args, **kwargs):
        """ generic UPnP service control method,
            which will be used if no soap_ACTIONNAME method
            in the server service control class can be found
        """
        try:
            action = self.actions[kwargs['soap_methodName']]
        except:
            return failure.Failure(errorCode(401))

        try:
            instance = int(kwargs['InstanceID'])
        except:
            instance = 0

        self.info("soap__generic", action, __name__, kwargs)
        del kwargs['soap_methodName']
        if( kwargs.has_key('X_UPnPClient') and
                kwargs['X_UPnPClient'] == 'XBox'):
            if(action.name == 'Browse' and
                    kwargs.has_key('ContainerID')):
                """ XXX: THIS IS SICK """
                kwargs['ObjectID'] = kwargs['ContainerID']
                del kwargs['ContainerID']

        in_arguments = action.get_in_arguments()
        for arg_name, arg in kwargs.iteritems():
            if arg_name.find('X_') == 0:
                continue
            l = [ a for a in in_arguments if arg_name == a.get_name()]
            if len(l) > 0:
                in_arguments.remove(l[0])
            else:
                self.critical('argument %s not valid for action %s' % (arg_name,action.name))
                return failure.Failure(errorCode(402))
        if len(in_arguments) > 0:
            self.critical('argument %s missing for action %s' %
                                ([ a.get_name() for a in in_arguments],action.name))
            return failure.Failure(errorCode(402))

        def callit( *args, **kwargs):
            #print 'callit args', args
            #print 'callit kwargs', kwargs
            result = {}
            callback = action.get_callback()
            if callback != None:
                return callback( **kwargs)
            return result

        def got_error(x):
            #print 'failure', x
            self.info('soap__generic error during call processing')
            return x

        # call plugin method for this action
        d = defer.maybeDeferred( callit, *args, **kwargs)
        d.addCallback( self.get_action_results, action, instance)
        d.addErrback(got_error)
        return d
