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

from .senml_record import SenmlRecord
from .senml_base import SenmlBase

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 sneml 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
    '''

    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.
        :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 value.
        :param value: only number allowed
        :return:
        '''
        self._check_value_type(value, "base_sum")
        self._base_sum = value

    @property
    def base_time(self):
        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 _process_incomming_data(self, records, naming_map):
        '''
        generic processor for incomming 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 = [x for x in self._data if x.name == item[naming_map['bn']]]
                else:
                    pack_el = [self]
                if len(pack_el) > 0:
                    cur_pack_el = pack_el[0]
                    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 = [x for x in cur_pack_el._data if x.name == item[naming_map['n']] ]
                if len(rec_el) > 0:
                    rec_el[0].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 = [x for x in self._data if x.name == item[naming_map['n']]]
                if len(rec_el) > 0:
                    rec_el[0].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 _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:
        '''
        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 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 list of records
        :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 = []
