import abc
from collections import OrderedDict

from .exceptions import InvalidInputChannelException
from .input import Input
from .data import NoData


def format_input_channels(_input_channel_ids):
    input_str = ", ".join(_input_channel_ids) if len(_input_channel_ids) > 0 else "No inputs"
    return "**Inputs:** " + input_str + "\n\n"


class _StaticInputChannelMaker(abc.ABCMeta):
    def __new__(mcs, name, bases, dct):

        for base in bases:
            for key, value in base.__dict__.items():
                if isinstance(value, Input):
                    if key not in dct:
                        dct[key] = value
        _input_channel_ids = ()
        for key, value in dct.items():
            if isinstance(value, Input):
                value.name = key
                _input_channel_ids += key,
        dct["_input_channel_ids"] = _input_channel_ids
        new_cls = super().__new__(mcs, name, bases, dct)
        if isinstance(new_cls.__doc__, str):
            new_cls.__doc__ = format_input_channels(_input_channel_ids) + "\n\n\n\n" + new_cls.__doc__
        else:
            new_cls.__doc__ = format_input_channels(_input_channel_ids)
        return new_cls


class StaticInputs(metaclass=_StaticInputChannelMaker):
    def __init__(self, **kwargs):
        self._inputs = OrderedDict()
        for key in self._input_channel_ids:
            if not getattr(self, key):
                self._inputs.update({key: self._null_input(key)})
        for key, value in kwargs.items():
            self[key] = value
        self.inputs_set_at_initialization = list(kwargs.keys())

    def _null_input(self, name):
        return NoData()

    @property
    def inputs(self):
        return self._inputs.copy()



def format_numpy(value):
    try:
        if len(value.shape) in [0, 1]:
            return repr(value)
        if value.shape[0] > 3 or value.shape[1] > 3:
            return "<ndarray: {row}x{col}, {type_}>".format(row=value.shape[0], col=value.shape[1], type_=value.dtype)
    except AttributeError:
        return value


class Callable:
    def __call__(self, *args, **kwargs):
        # template method
        if not ((len(args) == 0) or (len(kwargs) == 0)):
            raise Exception("Either pass a dictionary or keyword arguments but not both.")
        if len(args) + len(kwargs) >= 1:
            self.update_inputs(args[0] if len(args) == 1 else kwargs)
        for name, value in self._inputs.items():
            assert value is not NoData(), "Input \"{name}\" not set in {obj}.".format(name=name, obj=str(self))
        return self.run()

    def update_inputs(self, input_dict):
        for key, value in input_dict.items():
            self[key] = value

    def __repr__(self):
        ins = [str(id_) + "=" + str(format_numpy(val)) for id_, val in self._inputs.items()]
        return self.__class__.__name__ + "(" + ", ".join(ins) + ")"


class NameMixin:
    @property
    def name(self):
        try:
            return self._name
        except AttributeError:
            return self.__class__.__name__

    @name.setter
    def name(self, value):
        self._name = value


class Node(NameMixin, StaticInputs, Callable):
    """
    All units with attribute descriptors of the form
    ``x = Input()`` should derive from this class
    """
    def __setitem__(self, key, value):
        if key not in self._input_channel_ids:
            raise InvalidInputChannelException("\"" + key + "\"" +" in " + str(self))
        self._inputs.update({key: value})

    def __getitem__(self, name):
        return self._inputs[name]

    @property
    def input_names(self):
        return list(self._input_channel_ids)

    @abc.abstractmethod
    def run(self):
        pass

