# _  __  ____    _   _
# | |/ / |  _ \  | \ | |
# | ' /  | |_) | |  \| |
# | . \  |  __/  | |\  |
# |_|\_\ |_|     |_| \_|
#
# (c) 2018 KPN
# License: MIT license.
# Author: Jan Bogaerts
#
# pack object


from kpn_senml.senml_record import SenmlRecord
from kpn_senml.senml_base import SenmlBase
import json
import cbor2


class SenmlPackIterator:
    '''an iterator to walk over all records in a pack'''

    def __init__(self, list):
        self._list = list
        self._index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if(self._index > len(self._list)):
            res = self._list[self._index]
            self._index += 1
            return res
        else:
            raise StopIteration()

class SenmlPack(SenmlBase):
    '''
    represents a senml pack object. This can contain multiple records but also other (child) pack objects.
    When the pack object only contains records, it represents the data of a device.
    If the pack object has child pack objects, then it represents a gateway
    '''

    json_mappings = {'bn': 'bn', 'bt': 'bt', 'bu': 'bu', 'bv': 'bv', 'bs': 'bs',
                      'n': 'n', 'u': 'u', 'v': 'v', 'vs': 'vs', 'vb': 'vb', 'vd': 'vd', 's': 's', 't': 't', 'ut': 'ut'}

    def __init__(self, name, callback=None):
        '''
        initialize the object
        :param name: {string} the name of the pack
        '''
        self._data = []
        self.name = name
        self._base_value = None
        self._base_time = None
        self._base_sum = None
        self.base_unit = None
        self._parent = None                                 # a pack can also be the child of another pack.
        self.actuate = callback                             # actuate callback function

    def __iter__(self):
        return SenmlPackIterator(self._data)

    def __enter__(self):
        '''
        for supporting the 'with' statement
        :return: self
        '''
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        '''
        when destroyed in a 'with' statement, make certain that the item is removed from the parent list.
        :return: None
        '''
        if self._parent:
            self._parent.remove(self)

    @property
    def base_value(self):
        '''
        the base value of the pack. The value of the records will be subtracted by this value during rendering.
        While parsing, this value is added to the value of the records.
        :return: a number
        '''
        return self._base_value

    @base_value.setter
    def base_value(self, value):
        '''
        set the base value.
        :param value: only number allowed
        :return:
        '''
        self._check_value_type(value, "base_value")
        self._base_value = value


    @property
    def base_sum(self):
        '''
        the base sum of the pack.
        :return: a number
        '''
        return self._base_sum

    @base_sum.setter
    def base_sum(self, value):
        '''
        set the base sum.
        :param value: only number allowed
        :return:
        '''
        self._check_value_type(value, "base_sum")
        self._base_sum = value

    @property
    def base_time(self):
        '''
        Get the base time assigned to this pack object.
        While rendering, this value will be subtracted from the value of the records.
        :return: unix time stamp representing the base time
        '''
        return self._base_time

    @base_time.setter
    def base_time(self, value):
        self._check_value_type(value, "base_time")
        self._base_time = value

    def _check_value_type(self, value, field_name):
        '''
        checks if the type of value is allowed for senml
        :return: None, raisee exception if not ok.
        '''
        if (not value == None):
            if not (isinstance(value, int) or isinstance(value, float)):
                raise Exception("invalid type for " + field_name + ", only numbers allowed")


    def from_json(self, data):
        '''
        parse a json string and convert it to a senml pack structure
        :param data: a string containing json data.
        :return: None, will call the appropriate callback functions.
        '''
        records = json.loads(data)                                              # load the raw senml data
        self._process_incomming_data(records, SenmlPack.json_mappings)


    def _process_incomming_data(self, records, naming_map):
        '''
        generic processor for incoming data (actuators.
        :param records: the list of raw senml data, parsed from a json or cbor structure
        :param naming_map: translates cbor to json field names (when needed).
        :return: None
        '''
        cur_pack_el = self
        new_pack = False
        for item in records:
            if naming_map['bn'] in item:                                                    # ref to a pack element, either this or a child pack.
                if item[naming_map['bn']] != self.name:
                    pack_el = next( (x for x in self._data if x.name == item[naming_map['bn']]), None )
                else:
                    pack_el = self
                if pack_el:
                    cur_pack_el = pack_el
                    new_pack = False
                else:
                    device = SenmlPack(item[naming_map['bn']])
                    self._data.append(device)
                    cur_pack_el = device
                    new_pack = True

                if naming_map['bv'] in item:                                        # need to copy the base value assigned to the pack element so we can do proper conversion for actuators.
                    cur_pack_el.base_value = item[naming_map['bv']]

                rec_el = next( (x for x in cur_pack_el._data if x.name == item[naming_map['n']]), None )
                if rec_el:
                    rec_el.do_actuate(item, naming_map)
                elif new_pack:
                    self.do_actuate(item, naming_map, cur_pack_el)
                else:
                    cur_pack_el.do_actuate(item, naming_map)
            else:
                rec_el = next((x for x in self._data if x.name == item[naming_map['n']]), None)
                if rec_el:
                    rec_el.do_actuate(item, naming_map)
                elif new_pack:
                    self.do_actuate(item, naming_map, cur_pack_el)
                else:
                    cur_pack_el.do_actuate(item, naming_map)


    def do_actuate(self, raw, naming_map, device = None):
        '''
        called while parsing incoming data for a record that is not yet part of this pack object.
        adds a new record and raises the actuate callback of the pack with the newly created record as argument
        :param naming_map:
        :param device: optional: if the device was not found
        :param raw: the raw record definition, as found in the json structure. this still has invalid labels.
        :return: None
        '''
        rec = SenmlRecord(raw[naming_map['n']])
        if device:
            device.add(rec)
            rec._from_raw(raw, naming_map)
            if self.actuate:
                self.actuate(rec, device=device)
        else:
            self.add(rec)
            rec._from_raw(raw, naming_map)
            if self.actuate:
                self.actuate(rec, device=None)


    def to_json(self):
        '''
        render the content of this object to a string.
        :return: a string representing the senml pack object
        '''
        converted = []
        self._build_rec_dict(SenmlPack.json_mappings, converted)
        return json.dumps(converted)


    def _build_rec_dict(self, naming_map, appendTo):
        '''
        converts the object to a senml object with the proper naming in place.
        This can be recursive: a pack can contain other packs.
        :param naming_map: a dictionary used to pick the correct field names for either senml json or senml cbor
        :return: None
        '''
        internalList = []
        for item in self._data:
            item._build_rec_dict(naming_map,internalList)
        if len(internalList) > 0:
            first_rec = internalList[0]
        else:
            first_rec = {}
            internalList.append(first_rec)

        if self.name:
            first_rec[naming_map['bn']] = self.name
        if self.base_value:
            first_rec[naming_map['bv']] = self.base_value
        if self.base_unit:
            first_rec[naming_map['bu']] = self.base_unit
        if self.base_sum:
            first_rec[naming_map['bs']] = self.base_sum
        if self.base_time:
            first_rec[naming_map['bt']] = self.base_time
        appendTo.extend(internalList)

    def from_cbor(self, data):
        '''
        parse a cbor data byte array to a senml pack structure.
        :param data: a byte array.
        :return: None
        '''
        records = cbor2.loads(data)  # load the raw senml data
        naming_map = {'bn': -2, 'bt': -3, 'bu': -4, 'bv': -5, 'bs': -16,
                      'n': 0, 'u': 1, 'v': 2, 'vs': 3, 'vb': 4, 'vd': 8, 's': 5, 't': 6, 'ut': 7}
        self._process_incomming_data(records, naming_map)

    def to_cbor(self):
        '''
        render the content of this object to a cbor byte array
        :return: a byte array
        '''
        naming_map = {'bn': -2, 'bt': -3, 'bu': -4, 'bv': -5, 'bs': -16,
                      'n': 0, 'u': 1, 'v': 2, 'vs': 3, 'vb': 4, 'vd': 8, 's': 5, 't': 6, 'ut': 7}
        converted = []
        self._build_rec_dict(naming_map, converted)
        return cbor2.dumps(converted)

    def add(self, item):
        '''
        adds the item to the list of records
        :param item: {SenmlRecord} the item that needs to be added to the pack
        :return: None
        '''
        if not (isinstance(item, SenmlBase)):
            raise Exception('invalid type of param, SenmlRecord or SenmlPack expected')
        if not item._parent == None:
            raise Exception('item is already part of a pack')

        self._data.append(item)
        item._parent = self

    def remove(self, item):
        '''
        removes the item from the pack
        :param item: {SenmlRecord} the item that needs to be removed
        :return: None
        '''
        if not (isinstance(item, SenmlBase)):
            raise Exception('invalid type of param, SenmlRecord or SenmlPack expected')
        if not item._parent == self:
            raise Exception('item is not part of this pack')

        self._data.remove(item)
        item._parent = None

    def clear(self):
        '''
        clear the list of the pack
        :return: None
        '''
        for item in self._data:
            item._parent = None
        self._data = []