"""
model goal:
    - neuron encapsulated input and output connections to other neurons, as well
      as transformation function that determines which output "axons" send a
      signal based on which "dendrite" inputs received a signal.
    - "brain" structure contains neurons, providing signal entry points for
      simple signals to enter, and output points for simple signals to exit.
      - should allow for new connections to between neurons to be formed based
        on some rule-set.
      - should allow for new neurons to be formed based on some rule-set.
      - should provide functions for making a copy of itself, with and without
        "mutations"

Ideas:
    - L-system defining brain neuron construction? Possible good for building an
      initial structure, and then letting neuron adaptation to tweak system.

"""

import types


class Connection (object):
    """
    Base connection between neurons. Once a signal is transmitted, the
    connection looses its "charge", setting its signal to 0 (False).

    When async, this will need to cross process borders.
    Or use database/message-passing (a la ZMQ or something).
    """

    def __init__(self):
        self._signal = False

    def set_signal(self):
        """
        Charge this connection.
        """
        self._signal = True

    def get_signal(self):
        """
        Consume current signal charge.
        :return: Charge status of this connection: True if charged, False if not
        :rtype: bool
        """
        s = self._signal
        self._signal = False
        return s


class Neuron (object):
    """
    Computational neuron with multiple connections.

    Inputs come in via one or more connections. Based on where the inputs come
    from output, generated by a function, is output to different connections,
    determined by a separate function.

    :note: When async, each neuron could have its own internal "beat" which
        would allow for disparate signal reception from connected neurons.

    Neuron Valuation:
        For brain adaptation purposes, there must be (simple) rules as to which
    neurons have a greater or lesser chance to form new connections, break
    connections, or die. As for determining when brand new neurons are to be
    formed, I don't have a good idea yet.

    """
    CONNECTION_TYPE = Connection

    def __init__(self, t_func, fire_rate_hz=100):
        """
        Create a new neuron with no initial connections

        :param t_func: Function for transforming input signal pattern into an
            output signal pattern
        :type t_func: function

        :param fire_rate_hz: Fire rate in Hz. This will be for async
            functionality.
        :type fire_rate_hz: float

        """
        # function transforming input map to output map
        if not isinstance(t_func, (types.FunctionType, types.MethodType)):
            raise RuntimeError("Transformation function must be a function! "
                               "Given type: %s"
                               % type(t_func))
        self.transform_function = t_func

        # References to Axons of other Neurons we are connected to for signal
        # reception
        #: :type: list of Connection
        self.listening_to = []

        # References to Dendrites of other Neurons we are connected to for
        # signal transmission
        #: :type: list of Connection
        self.sending_to = []

        # ==================
        # Future async stuff
        # ------------------
        self.fire_rate_Hz = fire_rate_hz
        # transform fire rate based on some function
        self.fire_rate_transform = None

    def listen_to_neuron(self, other_n):
        """
        Register this neuron to listen to another neuron.
        :param other_n: Another neuron that we will listen to for signals.
        :type other_n: Neuron
        """
        self.listening_to.append(other_n.get_new_axon())

    def send_to_neuron(self, other_n):
        """
        Register this neuron to send signals to another neuron.
        :param other_n: Another neuron that we will send signals to.
        :type other_n: Neuron
        """
        self.sending_to.append(other_n.get_new_dendrite())

    def get_new_axon(self):
        """
        Form a new axon to connect to another neuron
        :return: A new axon connection (output)
        :rtype: Connection
        """
        c = self.CONNECTION_TYPE()
        self.sending_to.append(c)
        return c

    def get_new_dendrite(self):
        """
        Form a new dendrite connection to another neuron
        :return: A new dendrite connection (input)
        :rtype: Connection
        """
        c = self.CONNECTION_TYPE()
        self.listening_to.append(c)
        return c

    def activate(self):
        """
        Evaluate transformation function against input dendrite signals and send
        signals along appropriate axon connections.

        :note: In async, wait a prescribed amount of time for accumulation of
            non-synchronous signal propagation.
        """
        # produce value based on dendrite connections, first connection is
        # least-significant bit
        i_val = 0
        for i, axon in enumerate(self.listening_to):
            i_val += axon.get_signal() << i

        # execute transformation function given value
        o_val = self.transform_function(i_val)

        # set value as binary to axon connection starting with least-significant
        # bit in output value
        for dendrite in self.sending_to:
            if o_val & 1:
                dendrite.set_signal()
            o_val >>= 1