from functools import wraps


class AttributeDescription(object):
    def __init__(self, text, value=None, *args, **kwargs):
        self.name = None
        self.text = text
        self.value = value

    def __call__(self, attr, model):
        self.name = attr

    def __get__(self, obj, type=None): # pragma: no cover
        return self.value

    def __set__(self, obj, val): # pragma: no cover
        self.value = val


class Dimension(AttributeDescription):
    def __get__(self, obj, type=None):
        return obj._dims.get(self.name, None)

    def __set__(self, obj, value):
        obj._dims[self.name] = value


class Weights(AttributeDescription):
    def __init__(self, text, get_shape, init=None):
        self.name = None
        self.text = text
        self.get_shape = get_shape
        self.init = init

    def __get__(self, obj, type=None):
        key = (obj.id, self.name)
        if key in obj._mem:
            return obj._mem[key]
        else:
            shape = self.get_shape(obj)
            data = obj._mem.add(key, shape)
            if self.init is not None:
                self.init(data, obj.ops)
            return data

    def __set__(self, obj, val):
        data = obj._mem.get((obj.id, self.name))
        data[:] = val


class Gradient(AttributeDescription):
    def __init__(self, param_name):
        self.name = None
        self.text = "Gradient of %s" % param_name
        self.param_name = param_name

    def __get__(self, obj, type=None):
        key = (obj.id, self.name)
        if key in obj._mem:
            return obj._mem.get(key)
        else:
            param_key = (obj.id, self.param_name)
            grad = obj._mem.add_gradient(key, param_key)
            return grad

    def __set__(self, obj, val):
        data = obj._mem.get((obj.id, self.name))
        data[:] = val


class Synapses(Weights):
    pass


class Biases(Weights):
    pass


def attributes(**specs):
    if not specs: # pragma: no cover
        raise ValueError("Must describe at least one attribute")
    def wrapped(cls):
        cls.descriptions = dict(cls.descriptions)
        cls.descriptions.update(specs)
        for attr, desc in cls.descriptions.items():
            setattr(cls, attr, desc)
            desc.name = attr
        return cls
    return wrapped


def on_init(*callbacks):
    def wrapped(cls):
        cls.on_init_hooks = list(cls.on_init_hooks)
        cls.on_init_hooks.extend(callbacks)
        return cls
    return wrapped


def on_data(*callbacks):
    def wrapped(cls):
        cls.on_data_hooks = list(cls.on_data_hooks)
        cls.on_data_hooks.extend(callbacks)
        return cls
    return wrapped


def input(getter):
    def wrapped(cls):
        cls.describe_input = getter
        return cls
    return wrapped


def output(getter):
    def wrapped(cls):
        cls.describe_output = getter
        return cls
    return wrapped
