from enum import Enum

from .._ingenialink import ffi, lib
from ingenialink.utils._utils import *
from ..register import Register, REG_DTYPE, REG_ACCESS, REG_PHY, dtypes_ranges

import collections


def get_enums(enums, enums_count):
    """Obtain enumerations list of the register.

    Returns:
        array: Enumerations of the register.

    """
    aux_enums = []
    for i in range(enums_count):
        aux_dict = {
            'label': pstr(enums[i].label),
            'value': enums[i].value
        }
        aux_enums.append(aux_dict)
    return aux_enums


def get_range(reg_range, dtype):
    """Obtains register range.

    Returns:
        tuple: Register range (min, max), None if undefined.

    """

    if dtype == REG_DTYPE.S8:
        return reg_range.min.s8, reg_range.max.s8
    elif dtype == REG_DTYPE.U8:
        return reg_range.min.u8, reg_range.max.u8
    if dtype == REG_DTYPE.S16:
        return reg_range.min.s16, reg_range.max.s16
    elif dtype == REG_DTYPE.U16:
        return reg_range.min.u16, reg_range.max.u16
    if dtype == REG_DTYPE.S32:
        return reg_range.min.s32, reg_range.max.s32
    elif dtype == REG_DTYPE.U32:
        return reg_range.min.u32, reg_range.max.u32
    if dtype == REG_DTYPE.S64:
        return reg_range.min.s64, reg_range.max.s64
    elif dtype == REG_DTYPE.U64:
        return reg_range.min.u64, reg_range.max.u64
    elif dtype == REG_DTYPE.FLOAT:
        return reg_range.min.flt, reg_range.max.flt

    return None


def get_storage(storage, storage_valid, dtype):
    """Obtain register storage.

    Returns:
         int: Register storage.

    """
    if not storage_valid:
        return None

    if dtype == REG_DTYPE.S8:
        return storage.s8
    elif dtype == REG_DTYPE.U8:
        return storage.u8
    if dtype == REG_DTYPE.S16:
        return storage.s16
    elif dtype == REG_DTYPE.U16:
        return storage.u16
    if dtype == REG_DTYPE.S32:
        return storage.s32
    elif dtype == REG_DTYPE.U32:
        return storage.u32
    if dtype == REG_DTYPE.S64:
        return storage.s64
    elif dtype == REG_DTYPE.U64:
        return storage.u64
    elif dtype == REG_DTYPE.FLOAT:
        return storage.flt
    else:
        return None


def ipb_register_from_cffi(cffi_register):
    """Creates an IPBRegister instance from a CFFI register instance.

    Args:
        cffi_register (CData): CFFI instance of the register.

    Returns:
        IPBRegister: Instance of the newly created register.
    """
    units = None
    cyclic = None
    labels = None
    cat_id = None
    scat_id = None

    identifier = pstr(cffi_register.identifier)
    dtype = REG_DTYPE(cffi_register.dtype)
    access = REG_ACCESS(cffi_register.access)
    phy = REG_PHY(cffi_register.phy)
    address = cffi_register.address
    subnode = cffi_register.subnode
    storage = get_storage(cffi_register.storage, cffi_register.storage_valid, dtype)
    reg_range = get_range(cffi_register.range, dtype)
    enums_count = cffi_register.enums_count
    enums = get_enums(cffi_register.enums, enums_count)
    internal_use = cffi_register.internal_use

    if cffi_register.units != ffi.NULL:
        units = pstr(cffi_register.units)
    if cffi_register.cyclic != ffi.NULL:
        cyclic = pstr(cffi_register.cyclic)
    if cffi_register.cat_id != ffi.NULL:
        cat_id = pstr(cffi_register.cat_id)
    if cffi_register.scat_id != ffi.NULL:
        scat_id = pstr(cffi_register.scat_id)
    if cffi_register.labels != ffi.NULL:
        labels = LabelsDictionary._from_labels(cffi_register.labels)

    return IPBRegister(identifier, units, cyclic, dtype, access,
                       address, phy, subnode, storage, reg_range,
                       labels, enums, enums_count, cat_id, scat_id,
                       internal_use, cffi_register)


class IPBRegister(Register):
    """IPB Register.

    Args:
        identifier (str): Identifier.
        units (str): Units.
        cyclic (str): Cyclic typed register.
        dtype (REG_DTYPE): Data type.
        access (REG_ACCESS): Access type.
        address (int): Address.
        phy (REG_PHY, optional): Physical units.
        subnode (int): Subnode
        storage (any, optional): Storage.
        reg_range (tuple, optional): Range (min, max).
        labels (dict, optional): Register labels.
        enums (dict, optional): Enumeration values.
        cat_id (str, optional): Category ID.
        scat_id (str, optional): Sub-category ID.
        internal_use (int, optional): Internal use.

    Raises:
        TypeError: If any of the parameters has invalid type.

    """

    def __init__(self, identifier, units, cyclic, dtype, access, address,
                 phy=REG_PHY.NONE, subnode=1, storage=None, reg_range=None,
                 labels=None, enums=None, enums_count=0, cat_id=None, scat_id=None,
                 internal_use=0, c_reg=None):
        if labels is None:
            labels = {}
        if enums is None:
            enums = []
        super(IPBRegister, self).__init__(
            identifier, units, cyclic, dtype, access, phy, subnode, storage,
            reg_range, labels, enums, enums_count, cat_id, scat_id, internal_use)

        if not isinstance(dtype, REG_DTYPE):
            raise TypeError('Invalid data type')
        if not isinstance(access, REG_ACCESS):
            raise TypeError('Invalid access type')
        if not isinstance(phy, REG_PHY):
            raise TypeError('Invalid physical units type')
        if not cat_id and scat_id:
            raise ValueError('Sub-category requires a parent category')

        self._address = address
        self._labels = LabelsDictionary(labels)
        self.__update_range(dtype)
        if c_reg is None:
            self._reg = self.__create_c_reg()
        else:
            self._reg = c_reg

    def __repr__(self):
        """Obtain register object.

        Returns:
            str: Register information.

        """
        if self.cat_id:
            cat_info = self.cat_id
            if self.scat_id:
                cat_info += ' + ' + self.scat_id
        else:
            cat_info = 'Uncategorized'

        if self.storage and self.storage_valid:
            storage_info = self.storage
        else:
            storage_info = 'No storage'

        return '<Register: {}, {}, {}, {}, 0x{:08x}, {}{}, {}, {}, [], {},' \
               'ST: {}, [{}], {}>'.format(
                self.identifier,
                self.units,
                self.subnode,
                self.cyclic,
                self.address,
                self.dtype,
                ' ∊ ' + str(self.range) if self.range else '',
                self.access,
                self.phy,
                self.enums,
                self.enums_count,
                storage_info,
                cat_info,
                self.internal_use)

    def __create_c_reg(self):
        _reg = ffi.new('il_reg_t *')

        _reg.identifier = ffi.new("char[]", cstr(self.identifier))
        _reg.units = ffi.new("char[]", cstr(self.units))
        _reg.address = self.address
        _reg.subnode = self.subnode
        _reg.cyclic = ffi.new("char[]", cstr(self.cyclic))
        _reg.dtype = self.dtype.value
        _reg.access = self.access.value
        _reg.phy = self.phy.value
        _reg.internal_use = self.internal_use

        _reg.storage_valid = 0 if not self.storage else 1

        dtype_attr = {
            REG_DTYPE.U8: "u8",
            REG_DTYPE.S8: "s8",
            REG_DTYPE.U16: "u16",
            REG_DTYPE.S16: "s16",
            REG_DTYPE.U32: "u32",
            REG_DTYPE.S32: "s32",
            REG_DTYPE.U64: "u64",
            REG_DTYPE.S64: "s64",
            REG_DTYPE.FLOAT: "flt"
        }
        if self.dtype in REG_DTYPE and self.dtype in dtype_attr:
            attr_name = dtype_attr[self.dtype]
            if self.storage:
                if self.dtype == REG_DTYPE.FLOAT:
                    setattr(_reg.storage, attr_name, float(self.storage))
                else:
                    setattr(_reg.storage, attr_name, int(self.storage))
            setattr(_reg.range.min, attr_name, self.range[0])
            setattr(_reg.range.max, attr_name, self.range[1])
        else:
            _reg.storage_valid = 0

        _reg.labels = self._labels._labels
        _reg.enums_count = self.enums_count

        _reg.cat_id = ffi.NULL if not self.cat_id else cstr(self.cat_id)

        _reg.scat_id = ffi.NULL if not self.scat_id else cstr(self.scat_id)

        return _reg

    def __update_range(self, dtype):
        if dtype not in dtypes_ranges:
            return
        aux_range = (
            self.range[0] if self.range[0] is not None else dtypes_ranges[dtype]["min"],
            self.range[1] if self.range[1] is not None else dtypes_ranges[dtype]["max"],
        )
        self._range = aux_range

    @property
    def address(self):
        """int: Obtain register address."""
        return self._address


class LabelsDictionary(collections.MutableMapping):
    """Labels dictionary.

    Args:
        labels (dict, optional): Labels.

    Raises:
        ILCreationError: If the dictionary could not be created.

    """

    def __init__(self, labels=None):
        if labels is None:
            labels = {}
        _labels = lib.il_dict_labels_create()
        raise_null(_labels)

        self._labels = ffi.gc(_labels, lib.il_dict_labels_destroy)

        for lang, content in labels.items():
            lib.il_dict_labels_set(self._labels, cstr(lang), cstr(content))

        self._load_langs()

    @classmethod
    def _from_labels(cls, _labels):
        """Create a new class instance from an existing labels dictionary."""
        inst = cls.__new__(cls)
        inst._labels = _labels

        inst._load_langs()

        return inst

    def _load_langs(self):
        """Load languages from dictionary (cache)."""
        langs = lib.il_dict_labels_langs_get(self._labels)

        self._langs = []
        i = 0
        lang = langs[0]
        while lang != ffi.NULL:
            self._langs.append(pstr(lang))
            i += 1
            lang = langs[i]

        lib.il_dict_labels_langs_destroy(langs)

    def __getitem__(self, lang):
        content_p = ffi.new('char **')
        r = lib.il_dict_labels_get(self._labels, cstr(lang), content_p)
        raise_err(r)

        return pstr(content_p[0])

    def __setitem__(self, lang, content):
        lib.il_dict_labels_set(self._labels, cstr(lang), cstr(content))
        self._langs.append(lang)

    def __delitem__(self, lang):
        lib.il_dict_labels_del(self._labels, cstr(lang))
        self._langs.remove(lang)

    def __len__(self):
        return len(self._langs)

    def __iter__(self):
        return iter(self._langs)
