"""A Python emulation of LabVIEW's Cluster"""
from abc import ABC, abstractmethod
from collections.abc import Sequence
from numbers import Number
from enum import IntEnum
import re
import numpy as np

READONLY_ATTRIBUTES = [
    "ID",
    "type",
    "name",
    "description",
    "tip",
    "caption",
    "unitlabel",
]


class DataFlow(IntEnum):
    """Data flow direction"""

    CONTROL = 1
    IN = 1
    INDICATOR = 2
    OUT = 2
    UNKNOWN = 3


def valididentifier(item: str) -> bool:
    """Test 'item' for valid Python identifier"""
    return bool(re.match(r"^[a-zA-Z][\w]+$", item))


class LV_Control(ABC):
    """Abstract base class for LabVIEW control types

    Parameters
    ----------
    kwargs
    """

    # LV control's Label is 'name' and must be unique
    def __init__(self, **kwargs):
        self.name = kwargs.pop("name")
        for attr in READONLY_ATTRIBUTES:
            try:
                setattr(self, attr, kwargs.pop(attr))
            except KeyError:
                pass
        # Exported VI strings does not indicate dataflow direction
        self._dataflow = DataFlow.UNKNOWN

    @abstractmethod
    def __repr__(self):
        raise NotImplementedError

    @abstractmethod
    def __str__(self):
        raise NotImplementedError

    def __setattr__(self, item, value):
        if item in self.__dict__ and item in READONLY_ATTRIBUTES:
            raise AttributeError(f"can't set {item}")
        super().__setattr__(item, value)

    def __hash__(self):
        return hash(self.name)

    def __dir__(self):
        attrs = [a for a in self.__dict__ if not a.startswith("_")]
        return attrs

    def set_dataflow(self, direction: str) -> None:
        """Set the control's dataflow direction

        Parameters
        ----------
        direction : str {'control', 'in', 'indicator', 'out'}
        """
        self._dataflow = DataFlow[direction.upper()]

    def __eq__(self, other):
        if not isinstance(other, type(self)):
            return False
        tests = []
        for selfattr, otherattr in zip(self.__dict__.keys(), other.__dict__.keys()):
            tests.append(getattr(self, selfattr) == getattr(other, otherattr))
        return all(tests)


class Numeric(LV_Control):
    """Numeric"""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.value = kwargs.pop("value", 0.0)

    def __setattr__(self, item, value):
        if item == "value":
            if not isinstance(value, Number):
                raise TypeError(f"'{value}' not a number")
        super().__setattr__(item, value)

    def __repr__(self):
        return f"{self.value}"

    def __str__(self):
        return f"{self.value}"


class Boolean(LV_Control):
    """Boolean"""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.value = kwargs.pop("value", False)

    def __setattr__(self, item, value):
        if item == "value":
            if not isinstance(value, bool):
                raise TypeError(f"'{value}' not a boolean")
        super().__setattr__(item, value)

    def __repr__(self):
        return f"{self.value}"

    def __str__(self):
        return f"{self.value}"


class Array(LV_Control):
    """Array"""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.value = kwargs.pop("value", np.array([]))

    def __setattr__(self, item, value):
        if item == "value":
            if isinstance(value, str) or not hasattr(value, "__iter__"):
                raise TypeError(f"'{value}' not array like")
            value = np.array(value)
        super().__setattr__(item, value)

    def __repr__(self):
        return f"{self.value}"

    def __str__(self):
        return f"{self.value}"


class Cluster(LV_Control, Sequence):
    """Cluster - A Python emulation of LabVIEW's Cluster

    A cluster in LabVIEW appears as a key-value mapping but is actually a C-struct
    where the variables (controls) have a set order and are arranged in a contiguous
    block of bytes. The ActiveX 'SetControlValue' expects a Sequence such as a 'list'
    or 'tuple' that follows the same ordering as in LabVIEW.

    In Python, a cluster is more naturally represented as a 'dict'. This 'Cluster'
    combines features of lists and dicts to facilitate communication with LabVIEW
    clusters.

    Parameters
    ----------
    kwargs
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._ctrls = {}
        for name, attrs in kwargs.get("ctrls", {}).items():
            self._ctrls[name] = make_control(**attrs)

    def __getitem__(self, control):
        if isinstance(control, int):
            control = list(self._ctrls)[control]
        return self._ctrls[control]

    def __setitem__(self, control, value):
        if isinstance(control, int):
            control = list(self._ctrls)[control]
        self._ctrls[control].value = value
        values = [ctrl.value for ctrl in self._ctrls]
        self._setfp(self.name, values)

    def __setattr__(self, item, value):
        if "_ctrls" in self.__dict__ and item in self._ctrls:
            self._ctrls[item].value = value
        elif item == "value":
            for c, v in zip(self._ctrls, value):
                self._ctrls[c].value = v
        else:
            super().__setattr__(item, value)

    def __getattr__(self, item):
        if item in self._ctrls:
            value = self._ctrls[item]
        elif item == "value":
            value = [c.value for c in self._ctrls.values()]
        else:
            value = super().__getattribute__(item)
        return value

    def __iter__(self):
        for _, ctrl in self._ctrls.items():
            yield ctrl

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

    # pylint: disable=arguments-differ
    # Sequence.index start and stop parameters are recommended, not required
    def index(self, control):
        """Return the index of 'control'"""
        for idx, k in enumerate(self._ctrls):
            if k == control:
                return idx
        raise ValueError(f"{control} not in Cluster")

    def count(self, _):
        raise NotImplementedError

    def as_dict(self):
        """Return controls as a dict"""
        return self._ctrls

    def __repr__(self):
        return f"Cluster({self.as_dict()})"

    def __str__(self):
        return str(self.as_dict())

    def __contains__(self, control):
        return control in self._ctrls

    def __eq__(self, other):
        if not isinstance(other, type(self)):
            return False
        tests = []
        for selfctrl, otherctrl in zip(self._ctrls.keys(), other._ctrls.keys()):
            tests.append(selfctrl == otherctrl)
            tests.append(getattr(self, selfctrl) == getattr(other, otherctrl))
        return all(tests)

    def __bool__(self):
        return len(self) > 0

    def update(self, controls: dict):
        """Update from a dict"""
        self._ctrls.update(controls)

    def __dir__(self):
        attrs = super().__dir__()
        attrs = [a for a in attrs if not a.startswith("_")]
        ctrls = []
        for ctrl in self._ctrls:
            if valididentifier(ctrl):
                ctrls.append(ctrl)
            else:
                ctrls.append(f"['{ctrl}']")
        return attrs + ctrls


LVControl_LU = {
    "Numeric": Numeric,
    "Boolean": Boolean,
    "String": LV_Control,
    "Path": LV_Control,
    "Time Stamp": LV_Control,
    "Waveform Graph": LV_Control,
    "Enum": LV_Control,
    "IVI Logical Name": LV_Control,
    "Slide": Numeric,
    "Array": Array,
    "Cluster": Cluster,
    "Measurement Data": Cluster,
}


def make_control(**attrs: dict) -> LV_Control:
    """Make LV_Control from VI strings attributes"""
    LV_Control_cls = LVControl_LU[attrs.pop("type")]
    return LV_Control_cls(**attrs)
