
# Slixmpp: The Slick XMPP Library
# Implementation of xeps for Internet of Things
# http://wiki.xmpp.org/web/Tech_pages/IoT_systems
# Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
from slixmpp import Iq, Message
from slixmpp.xmlstream import register_stanza_plugin, ElementBase
import re

class Sensordata(ElementBase):
    """ Placeholder for the namespace, not used as a stanza """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'sensordata'
    plugin_attrib = name
    interfaces = set()

class FieldTypes():
    """
    All field types are optional booleans that default to False
    """
    field_types = {'momentary', 'peak', 'status', 'computed', 'identity',
                   'historicalSecond', 'historicalMinute', 'historicalHour',
                   'historicalDay', 'historicalWeek', 'historicalMonth',
                   'historicalQuarter', 'historicalYear', 'historicalOther'}

class FieldStatus():
    """
    All field statuses are optional booleans that default to False
    """
    field_status = {'missing', 'automaticEstimate', 'manualEstimate',
                    'manualReadout', 'automaticReadout', 'timeOffset',
                    'warning', 'error', 'signed', 'invoiced', 'endOfSeries',
                    'powerFailure', 'invoiceConfirmed'}

class Request(ElementBase):
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'req'
    plugin_attrib = name
    interfaces = {'seqnr','nodes','fields','serviceToken','deviceToken','userToken','from','to','when','historical','all'}
    interfaces.update(FieldTypes.field_types)
    _flags = {'serviceToken','deviceToken','userToken','from','to','when','historical','all'}
    _flags.update(FieldTypes.field_types)

    def __init__(self, xml=None, parent=None):
        ElementBase.__init__(self, xml, parent)
        self._nodes = set()
        self._fields = set()

    def setup(self, xml=None):
        """
        Populate the stanza object using an optional XML object.

        Overrides ElementBase.setup

        Caches item information.

        Arguments:
            xml -- Use an existing XML object for the stanza's values.
        """
        ElementBase.setup(self, xml)
        self._nodes = {node['nodeId'] for node in self['nodes']}
        self._fields = {field['name'] for field in self['fields']}

    def _get_flags(self):
        """
        Helper function for getting of flags. Returns all flags in
        dictionary format: { "flag name": "flag value" ... }
        """
        flags = {}
        for f in self._flags:
            if not self[f] == "":
                flags[f] = self[f]
        return flags

    def _set_flags(self, flags):
        """
        Helper function for setting of flags.

        Arguments:
            flags -- Flags in dictionary format: { "flag name": "flag value" ... }
        """
        for f in self._flags:
            if flags is not None and f in flags:
                self[f] = flags[f]
            else:
                self[f] = None

    def add_node(self, nodeId, sourceId=None, cacheType=None):
        """
        Add a new node element. Each item is required to have a
        nodeId, but may also specify a sourceId value and cacheType.

        Arguments:
            nodeId    -- The ID for the node.
            sourceId  -- [optional] identifying the data source controlling the device
            cacheType -- [optional] narrowing down the search to a specific kind of node
        """
        if nodeId not in self._nodes:
            self._nodes.add((nodeId))
            node = RequestNode(parent=self)
            node['nodeId'] = nodeId
            node['sourceId'] = sourceId
            node['cacheType'] = cacheType
            self.iterables.append(node)
            return node
        return None

    def del_node(self, nodeId):
        """
        Remove a single node.

        Arguments:
            nodeId  -- Node ID of the item to remove.
        """
        if nodeId in self._nodes:
            nodes = [i for i in self.iterables if isinstance(i, RequestNode)]
            for node in nodes:
                if node['nodeId'] == nodeId:
                    self.xml.remove(node.xml)
                    self.iterables.remove(node)
                    return True
        return False

    def get_nodes(self):
        """Return all nodes."""
        nodes = []
        for node in self['substanzas']:
            if isinstance(node, RequestNode):
                nodes.append(node)
        return nodes

    def set_nodes(self, nodes):
        """
        Set or replace all nodes. The given nodes must be in a
        list or set where each item is a tuple of the form:
            (nodeId, sourceId, cacheType)

        Arguments:
            nodes -- A series of nodes in tuple format.
        """
        self.del_nodes()
        for node in nodes:
            if isinstance(node, RequestNode):
                self.add_node(node['nodeId'], node['sourceId'], node['cacheType'])
            else:
                nodeId, sourceId, cacheType = node
                self.add_node(nodeId, sourceId, cacheType)

    def del_nodes(self):
        """Remove all nodes."""
        self._nodes = set()
        nodes = [i for i in self.iterables if isinstance(i, RequestNode)]
        for node in nodes:
            self.xml.remove(node.xml)
            self.iterables.remove(node)


    def add_field(self, name):
        """
        Add a new field element. Each item is required to have a
        name.

        Arguments:
            name  -- The name of the field.
        """
        if name not in self._fields:
            self._fields.add((name))
            field = RequestField(parent=self)
            field['name'] = name
            self.iterables.append(field)
            return field
        return None

    def del_field(self, name):
        """
        Remove a single field.

        Arguments:
            name  -- name of field to remove.
        """
        if name in self._fields:
            fields = [i for i in self.iterables if isinstance(i, RequestField)]
            for field in fields:
                if field['name'] == name:
                    self.xml.remove(field.xml)
                    self.iterables.remove(field)
                    return True
        return False

    def get_fields(self):
        """Return all fields."""
        fields = []
        for field in self['substanzas']:
            if isinstance(field, RequestField):
                fields.append(field)
        return fields

    def set_fields(self, fields):
        """
        Set or replace all fields. The given fields must be in a
        list or set where each item is RequestField or string

        Arguments:
            fields -- A series of fields in RequestField or string format.
        """
        self.del_fields()
        for field in fields:
            if isinstance(field, RequestField):
                self.add_field(field['name'])
            else:
                self.add_field(field)

    def del_fields(self):
        """Remove all fields."""
        self._fields = set()
        fields = [i for i in self.iterables if isinstance(i, RequestField)]
        for field in fields:
            self.xml.remove(field.xml)
            self.iterables.remove(field)


class RequestNode(ElementBase):
    """ Node element in a request """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'node'
    plugin_attrib = name
    interfaces = {'nodeId','sourceId','cacheType'}

class RequestField(ElementBase):
    """ Field element in a request """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'field'
    plugin_attrib = name
    interfaces = {'name'}

class Accepted(ElementBase):
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'accepted'
    plugin_attrib = name
    interfaces = {'seqnr','queued'}

class Started(ElementBase):
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'started'
    plugin_attrib = name
    interfaces = {'seqnr'}

class Failure(ElementBase):
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'failure'
    plugin_attrib = name
    interfaces = {'seqnr','done'}

class Error(ElementBase):
    """ Error element in a request failure """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'error'
    plugin_attrib = name
    interfaces = {'nodeId','timestamp','sourceId','cacheType','text'}

    def get_text(self):
        """Return then contents inside the XML tag."""
        return self.xml.text

    def set_text(self, value):
        """Set then contents inside the XML tag.

        :param value: string
        """

        self.xml.text = value
        return self

    def del_text(self):
        """Remove the contents inside the XML tag."""
        self.xml.text = ""
        return self

class Rejected(ElementBase):
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'rejected'
    plugin_attrib = name
    interfaces = {'seqnr','error'}
    sub_interfaces = {'error'}

class Fields(ElementBase):
    """ Fields element, top level in a response message with data """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'fields'
    plugin_attrib = name
    interfaces = {'seqnr','done','nodes'}

    def __init__(self, xml=None, parent=None):
        ElementBase.__init__(self, xml, parent)
        self._nodes = set()

    def setup(self, xml=None):
        """
        Populate the stanza object using an optional XML object.

        Overrides ElementBase.setup

        Caches item information.

        Arguments:
            xml -- Use an existing XML object for the stanza's values.
        """
        ElementBase.setup(self, xml)
        self._nodes = {node['nodeId'] for node in self['nodes']}


    def add_node(self, nodeId, sourceId=None, cacheType=None, substanzas=None):
        """
        Add a new node element. Each item is required to have a
        nodeId, but may also specify a sourceId value and cacheType.

        Arguments:
            nodeId  -- The ID for the node.
            sourceId  -- [optional] identifying the data source controlling the device
            cacheType -- [optional] narrowing down the search to a specific kind of node
        """
        if nodeId not in self._nodes:
            self._nodes.add((nodeId))
            node = FieldsNode(parent=self)
            node['nodeId'] = nodeId
            node['sourceId'] = sourceId
            node['cacheType'] = cacheType
            if substanzas is not None:
                node.set_timestamps(substanzas)

            self.iterables.append(node)
            return node
        return None

    def del_node(self, nodeId):
        """
        Remove a single node.

        Arguments:
            nodeId  -- Node ID of the item to remove.
        """
        if nodeId in self._nodes:
            nodes = [i for i in self.iterables if isinstance(i, FieldsNode)]
            for node in nodes:
                if node['nodeId'] == nodeId:
                    self.xml.remove(node.xml)
                    self.iterables.remove(node)
                    return True
        return False

    def get_nodes(self):
        """Return all nodes."""
        nodes = []
        for node in self['substanzas']:
            if isinstance(node, FieldsNode):
                nodes.append(node)
        return nodes

    def set_nodes(self, nodes):
        """
        Set or replace all nodes. The given nodes must be in a
        list or set where each item is a tuple of the form:
            (nodeId, sourceId, cacheType)

        Arguments:
            nodes -- A series of nodes in tuple format.
        """
        #print(str(id(self)) + " set_nodes: got " + str(nodes))
        self.del_nodes()
        for node in nodes:
            if isinstance(node, FieldsNode):
                self.add_node(node['nodeId'], node['sourceId'], node['cacheType'], substanzas=node['substanzas'])
            else:
                nodeId, sourceId, cacheType = node
                self.add_node(nodeId, sourceId, cacheType)

    def del_nodes(self):
        """Remove all nodes."""
        self._nodes = set()
        nodes = [i for i in self.iterables if isinstance(i, FieldsNode)]
        for node in nodes:
            self.xml.remove(node.xml)
            self.iterables.remove(node)


class FieldsNode(ElementBase):
    """ Node element in response fields """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'node'
    plugin_attrib = name
    interfaces = {'nodeId','sourceId','cacheType','timestamps'}

    def __init__(self, xml=None, parent=None):
        ElementBase.__init__(self, xml, parent)
        self._timestamps = set()

    def setup(self, xml=None):
        """
        Populate the stanza object using an optional XML object.

        Overrides ElementBase.setup

        Caches item information.

        Arguments:
            xml -- Use an existing XML object for the stanza's values.
        """
        ElementBase.setup(self, xml)
        self._timestamps = {ts['value'] for ts in self['timestamps']}

    def add_timestamp(self, timestamp, substanzas=None):
        """
        Add a new timestamp element.

        Arguments:
            timestamp  -- The timestamp in ISO format.
        """
        #print(str(id(self)) + " add_timestamp: " + str(timestamp))

        if timestamp not in self._timestamps:
            self._timestamps.add((timestamp))
            ts = Timestamp(parent=self)
            ts['value'] = timestamp
            if substanzas is not None:
                ts.set_datas(substanzas)
                #print("add_timestamp with substanzas: " + str(substanzas))
            self.iterables.append(ts)
            #print(str(id(self)) + " added_timestamp: " + str(id(ts)))
            return ts
        return None

    def del_timestamp(self, timestamp):
        """
        Remove a single timestamp.

        Arguments:
            timestamp  -- timestamp (in ISO format) of the item to remove.
        """
        #print("del_timestamp: ")
        if timestamp in self._timestamps:
            timestamps = [i for i in self.iterables if isinstance(i, Timestamp)]
            for ts in timestamps:
                if ts['value'] == timestamp:
                    self.xml.remove(ts.xml)
                    self.iterables.remove(ts)
                    return True
        return False

    def get_timestamps(self):
        """Return all timestamps."""
        #print(str(id(self)) + " get_timestamps: ")
        timestamps = []
        for timestamp in self['substanzas']:
            if isinstance(timestamp, Timestamp):
                timestamps.append(timestamp)
        return timestamps

    def set_timestamps(self, timestamps):
        """
        Set or replace all timestamps. The given timestamps must be in a
        list or set where each item is a timestamp

        Arguments:
            timestamps -- A series of timestamps.
        """
        #print(str(id(self)) + " set_timestamps: got " + str(timestamps))
        self.del_timestamps()
        for timestamp in timestamps:
            #print("set_timestamps: subset " + str(timestamp))
            #print("set_timestamps: subset.substanzas " + str(timestamp['substanzas']))
            if isinstance(timestamp, Timestamp):
                self.add_timestamp(timestamp['value'], substanzas=timestamp['substanzas'])
            else:
                #print("set_timestamps: got " + str(timestamp))
                self.add_timestamp(timestamp)

    def del_timestamps(self):
        """Remove all timestamps."""
        #print(str(id(self)) + " del_timestamps: ")
        self._timestamps = set()
        timestamps = [i for i in self.iterables if isinstance(i, Timestamp)]
        for timestamp in timestamps:
            self.xml.remove(timestamp.xml)
            self.iterables.remove(timestamp)

class Field(ElementBase):
    """
    Field element in response Timestamp. This is a base class,
    all instances of fields added to Timestamp must be of types:
        DataNumeric
        DataString
        DataBoolean
        DataDateTime
        DataTimeSpan
        DataEnum
    """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'field'
    plugin_attrib = name
    interfaces = {'name','module','stringIds'}
    interfaces.update(FieldTypes.field_types)
    interfaces.update(FieldStatus.field_status)

    _flags = set()
    _flags.update(FieldTypes.field_types)
    _flags.update(FieldStatus.field_status)

    def set_stringIds(self, value):
        """Verifies stringIds according to regexp from specification XMPP-0323.

        :param value: string
        """

        pattern = re.compile(r"^\d+([|]\w+([.]\w+)*([|][^,]*)?)?(,\d+([|]\w+([.]\w+)*([|][^,]*)?)?)*$")
        if pattern.match(value) is not None:
            self.xml.stringIds = value
        else:
            # Bad content, add nothing
            pass

        return self

    def _get_flags(self):
        """
        Helper function for getting of flags. Returns all flags in
        dictionary format: { "flag name": "flag value" ... }
        """
        flags = {}
        for f in self._flags:
            if not self[f] == "":
                flags[f] = self[f]
        return flags

    def _set_flags(self, flags):
        """
        Helper function for setting of flags.

        Arguments:
            flags -- Flags in dictionary format: { "flag name": "flag value" ... }
        """
        for f in self._flags:
            if flags is not None and f in flags:
                self[f] = flags[f]
            else:
                self[f] = None

    def _get_typename(self):
        return "invalid type, use subclasses!"


class Timestamp(ElementBase):
    """ Timestamp element in response Node """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'timestamp'
    plugin_attrib = name
    interfaces = {'value','datas'}

    def __init__(self, xml=None, parent=None):
        ElementBase.__init__(self, xml, parent)
        self._datas = set()

    def setup(self, xml=None):
        """
        Populate the stanza object using an optional XML object.

        Overrides ElementBase.setup

        Caches item information.

        Arguments:
            xml -- Use an existing XML object for the stanza's values.
        """
        ElementBase.setup(self, xml)
        self._datas = {data['name'] for data in self['datas']}

    def add_data(self, typename, name, value, module=None, stringIds=None, unit=None, dataType=None, flags=None):
        """
        Add a new data element.

        Arguments:
            typename   -- The type of data element (numeric, string, boolean, dateTime, timeSpan or enum)
            value      -- The value of the data element
            module     -- [optional] language module to use for the data element
            stringIds  -- [optional] The stringIds used to find associated text in the language module
            unit       -- [optional] The unit. Only applicable for type numeric
            dataType   -- [optional] The dataType. Only applicable for type enum
        """
        if name not in self._datas:
            dataObj = None
            if typename == "numeric":
                dataObj = DataNumeric(parent=self)
                dataObj['unit'] = unit
            elif typename == "string":
                dataObj = DataString(parent=self)
            elif typename == "boolean":
                dataObj = DataBoolean(parent=self)
            elif typename == "dateTime":
                dataObj = DataDateTime(parent=self)
            elif typename == "timeSpan":
                dataObj = DataTimeSpan(parent=self)
            elif typename == "enum":
                dataObj = DataEnum(parent=self)
                dataObj['dataType'] = dataType

            dataObj['name'] = name
            dataObj['value'] = value
            dataObj['module'] = module
            dataObj['stringIds'] = stringIds

            if flags is not None:
                dataObj._set_flags(flags)

            self._datas.add(name)
            self.iterables.append(dataObj)
            return dataObj
        return None

    def del_data(self, name):
        """
        Remove a single data element.

        Arguments:
            data_name  -- The data element name to remove.
        """
        if name in self._datas:
            datas = [i for i in self.iterables if isinstance(i, Field)]
            for data in datas:
                if data['name'] == name:
                    self.xml.remove(data.xml)
                    self.iterables.remove(data)
                    return True
        return False

    def get_datas(self):
        """ Return all data elements. """
        datas = []
        for data in self['substanzas']:
            if isinstance(data, Field):
                datas.append(data)
        return datas

    def set_datas(self, datas):
        """
        Set or replace all data elements. The given elements must be in a
        list or set where each item is a data element (numeric, string, boolean, dateTime, timeSpan or enum)

        Arguments:
            datas -- A series of data elements.
        """
        self.del_datas()
        for data in datas:
            self.add_data(typename=data._get_typename(), name=data['name'], value=data['value'], module=data['module'], stringIds=data['stringIds'], unit=data['unit'], dataType=data['dataType'], flags=data._get_flags())

    def del_datas(self):
        """Remove all data elements."""
        self._datas = set()
        datas = [i for i in self.iterables if isinstance(i, Field)]
        for data in datas:
            self.xml.remove(data.xml)
            self.iterables.remove(data)

class DataNumeric(Field):
    """
    Field data of type numeric.
    Note that the value is expressed as a string.
    """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'numeric'
    plugin_attrib = name
    interfaces = {'value', 'unit'}
    interfaces.update(Field.interfaces)

    def _get_typename(self):
        return "numeric"

class DataString(Field):
    """
    Field data of type string
    """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'string'
    plugin_attrib = name
    interfaces = {'value'}
    interfaces.update(Field.interfaces)

    def _get_typename(self):
        return "string"

class DataBoolean(Field):
    """
    Field data of type boolean.
    Note that the value is expressed as a string.
    """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'boolean'
    plugin_attrib = name
    interfaces = {'value'}
    interfaces.update(Field.interfaces)

    def _get_typename(self):
        return "boolean"

class DataDateTime(Field):
    """
    Field data of type dateTime.
    Note that the value is expressed as a string.
    """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'dateTime'
    plugin_attrib = name
    interfaces = {'value'}
    interfaces.update(Field.interfaces)

    def _get_typename(self):
        return "dateTime"

class DataTimeSpan(Field):
    """
    Field data of type timeSpan.
    Note that the value is expressed as a string.
    """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'timeSpan'
    plugin_attrib = name
    interfaces = {'value'}
    interfaces.update(Field.interfaces)

    def _get_typename(self):
        return "timeSpan"

class DataEnum(Field):
    """
    Field data of type enum.
    Note that the value is expressed as a string.
    """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'enum'
    plugin_attrib = name
    interfaces = {'value', 'dataType'}
    interfaces.update(Field.interfaces)

    def _get_typename(self):
        return "enum"

class Done(ElementBase):
    """ Done element used to signal that all data has been transferred """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'done'
    plugin_attrib = name
    interfaces = {'seqnr'}

class Cancel(ElementBase):
    """ Cancel element used to signal that a request shall be cancelled """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'cancel'
    plugin_attrib = name
    interfaces = {'seqnr'}

class Cancelled(ElementBase):
    """ Cancelled element used to signal that cancellation is confirmed """
    namespace = 'urn:xmpp:iot:sensordata'
    name = 'cancelled'
    plugin_attrib = name
    interfaces = {'seqnr'}


register_stanza_plugin(Iq, Request)
register_stanza_plugin(Request, RequestNode, iterable=True)
register_stanza_plugin(Request, RequestField, iterable=True)

register_stanza_plugin(Iq, Accepted)
register_stanza_plugin(Message, Failure)
register_stanza_plugin(Failure, Error)

register_stanza_plugin(Iq, Rejected)

register_stanza_plugin(Message, Fields)
register_stanza_plugin(Fields, FieldsNode, iterable=True)
register_stanza_plugin(FieldsNode, Timestamp, iterable=True)
register_stanza_plugin(Timestamp, Field, iterable=True)
register_stanza_plugin(Timestamp, DataNumeric, iterable=True)
register_stanza_plugin(Timestamp, DataString, iterable=True)
register_stanza_plugin(Timestamp, DataBoolean, iterable=True)
register_stanza_plugin(Timestamp, DataDateTime, iterable=True)
register_stanza_plugin(Timestamp, DataTimeSpan, iterable=True)
register_stanza_plugin(Timestamp, DataEnum, iterable=True)

register_stanza_plugin(Message, Started)

register_stanza_plugin(Iq, Cancel)
register_stanza_plugin(Iq, Cancelled)
