"""
  Author: Thomas G. Close (tclose@oist.jp)
  Copyright: 2012-2014 Thomas G. Close.
  License: This file is part of the "NineLine" package, which is released under
           the MIT Licence, see LICENSE for details.
"""
from __future__ import absolute_import
from builtins import next
from builtins import str
from past.builtins import basestring
from builtins import object
from collections import namedtuple, defaultdict
from itertools import chain
import quantities as pq
import neo
from nineml.user import Property
from pype9.exceptions import Pype9RuntimeError
from .values import get_pyNN_value
import os.path
import nineml
from nineml import units as un
from pyNN.parameters import Sequence
import pyNN.standardmodels
from nineml import Document
from nineml.exceptions import NineMLNameError
from nineml.user.multi import (
    MultiDynamicsProperties, append_namespace, BasePortExposure)
from nineml.abstraction import StateVariable
from nineml.user import (
    ComponentArray as ComponentArray9ML,
    EventConnectionGroup as EventConnectionGroup9ML,
    AnalogConnectionGroup as AnalogConnectionGroup9ML,
    Selection as Selection9ML,
    Concatenate as Concatenate9ML)
from pype9.exceptions import Pype9UnflattenableSynapseException
from .connectivity import InversePyNNConnectivity
from ..cells import (
    MultiDynamicsWithSynapsesProperties, ConnectionPropertySet,
    SynapseProperties)
from pype9.exceptions import Pype9UsageError, Pype9NameError


_REQUIRED_SIM_PARAMS = ['timestep', 'min_delay', 'max_delay', 'temperature']


class Network(object):
    """
    Constructs a network simulation, generating and compiling dynamics classes
    as required (depending on the 'build_mode' option). The populations and
    projections of the network are flattened so that the synapse projections
    are included in the cell dynamics and the connection groups are just
    static connections.

    Parameters
    ----------
    nineml_model : nineml.Network | nineml.Document | URL
        A 9ML-Python model of a network (or Document containing
        populations and projections for 9MLv1) or a URL referring to a 9ML
        model.
    """

    # Name given to the "cell" component of the cell dynamics + linear synapse
    # dynamics multi-dynamics
    CELL_COMP_NAME = 'cell'

    def __init__(self, nineml_model, build_mode='lazy', **kwargs):
        if isinstance(nineml_model, basestring):
            nineml_model = nineml.read(nineml_model).as_network(
                name=os.path.splitext(os.path.basename(nineml_model))[0])
        elif isinstance(nineml_model, Document):
            if nineml_model.url is not None:
                name = os.path.splitext(os.path.basename(nineml_model.url))[0]
            else:
                name = "Anonymous"
            nineml_model = nineml_model.as_network(name=name)
        self._nineml = nineml_model.clone()
        # Get RNG for random distribution values and connectivity
        rng = self.Simulation.active().properties_rng
        if build_mode != 'build_only':
            self.nineml.resample_connectivity(
                connectivity_class=self.ConnectivityClass, rng=rng)
        (flat_comp_arrays, flat_conn_groups,
         flat_selections) = self._flatten_to_arrays_and_conns(self._nineml)
        self._component_arrays = {}
        # Build the PyNN populations
        # Add build args to distinguish models built for this network as
        # opposed to other networks
        build_url = kwargs.pop('build_url', nineml_model.url)
        build_version = nineml_model.name + kwargs.pop('build_version', '')
        for name, comp_array in flat_comp_arrays.items():
            self._component_arrays[name] = self.ComponentArrayClass(
                comp_array, build_mode=build_mode,
                build_url=build_url, build_version=build_version, **kwargs)
        self._selections = {}
        # Build the PyNN Selections
        for selection in flat_selections.values():
            # TODO: Assumes that selections are only concatenations (which is
            #       true for 9MLv1.0 but not v2.0)
            self._selections[selection.name] = self.SelectionClass(
                selection, *[self.component_array(p.name)
                             for p in selection.populations])
        if build_mode != 'build_only':
            # Set the connectivity objects of the projections to the
            # PyNNConnectivity class
            if self.nineml.connectivity_has_been_sampled():
                raise Pype9RuntimeError(
                    "Connections have already been sampled, please reset them"
                    " using 'resample_connectivity' before constructing "
                    "network")
            self._connection_groups = {}
            for name, conn_group in flat_conn_groups.items():
                try:
                    source = self._component_arrays[conn_group.source.name]
                except KeyError:
                    source = self._selections[conn_group.source.name]
                try:
                    destination = self._component_arrays[
                        conn_group.destination.name]
                except KeyError:
                    destination = self._selections[conn_group.destination.name]
                self._connection_groups[name] = self.ConnectionGroupClass(
                    conn_group, source=source, destination=destination)
            self._finalise_construction()

    def _finalise_construction(self):
        """
        Can be overriden by deriving classes to do any simulator-specific
        finalisation that is required
        """
        pass

    @property
    def nineml(self):
        return self._nineml

    @property
    def component_arrays(self):
        "Iterate through component arrays"
        return iter(self._component_arrays.values())

    @property
    def connection_groups(self):
        "Iterate through connection_groups"
        return iter(self._connection_groups.values())

    @property
    def selections(self):
        "Iterate through selections"
        return iter(self._selections.values())

    def component_array(self, name):
        """
        Returns the component array matching the given name

        Parameters
        ----------
        name : str
            Name of the component array
        """
        try:
            return self._component_arrays[name]
        except KeyError:
            raise Pype9NameError(
                "No component array named '{}' (possible '{}')"
                .format(name, "', '".join(self.component_array_names)))

    def connection_group(self, name):
        """
        Returns the connection group matching the given name

        Parameters
        ----------
        name : str
            Name of the component array
        """
        try:
            return self._connection_groups[name]
        except KeyError:
            raise Pype9NameError(
                "No connection group named '{}' (possible '{}')"
                .format(name, "', '".join(self.connection_group_names)))

    def selection(self, name):
        """
        Returns the selection matching the given name

        Parameters
        ----------
        name : str
            Name of the selection
        """
        try:
            return self._selections[name]
        except KeyError:
            raise Pype9NameError(
                "No selection named '{}' (possible '{}')"
                .format(name, "', '".join(self.selection_names)))

    @property
    def num_component_arrays(self):
        return len(self._component_arrays)

    @property
    def num_connection_groups(self):
        return len(self._connection_groups)

    @property
    def num_selections(self):
        return len(self._selections)

    @property
    def component_array_names(self):
        return list(self._component_arrays.keys())

    @property
    def connection_group_names(self):
        return list(self._connection_groups.keys())

    @property
    def selection_names(self):
        return list(self._selections.keys())

    def save_connections(self, output_dir):
        """
        Saves generated connections to output directory

        @param output_dir:
        """
        for conn_grp in self.connection_groups.values():
            if isinstance(conn_grp.synapse_type,
                          pyNN.standardmodels.synapses.ElectricalSynapse):
                attributes = 'weight'
            else:
                attributes = 'all'
            conn_grp.save(attributes, os.path.join(
                output_dir, conn_grp.label + '.proj'), format='list',
                gather=True)

    def record(self, variable, t_start=None):  # @UnusedVariable
        """
        Record variable from complete network
        """
        for comp_array in self.component_arrays:
            comp_array.record(variable)

    def write_data(self, file_prefix, **kwargs):
        """
        Record all spikes generated in the network

        @param filename: The prefix for every population files before the
                         popluation name. The suffix '.spikes' will be
                         appended to the filenames as well.
        """
        # Add a dot to separate the prefix from the population label if it
        # doesn't already have one and isn't a directory
        if (not os.path.isdir(file_prefix) and
            not file_prefix.endswith('.') and
              not file_prefix.endswith(os.path.sep)):
            file_prefix += '.'
        for comp_array in self.component_arrays.values():
            # @UndefinedVariable
            comp_array.write_data(file_prefix + comp_array.name + '.pkl',
                                  **kwargs)

    @classmethod
    def _flatten_synapse(cls, projection_model):
        """
        Flattens the reponse and plasticity dynamics into a single synapse
        element (will be 9MLv2 format) and updates the port connections
        to match the changed object.
        """
        role2name = {'response': 'psr', 'plasticity': 'pls'}
        syn_comps = {
            role2name['response']: projection_model.response,
            role2name['plasticity']: projection_model.plasticity}
        # Get all projection port connections that don't project to/from
        # the "pre" population and convert them into local MultiDynamics
        # port connections of the synapse
        syn_internal_conns = (
            pc.__class__(
                sender_name=role2name[pc.sender_role],
                receiver_name=role2name[pc.receiver_role],
                send_port_name=pc.send_port_name,
                receive_port_name=pc.receive_port_name)
            for pc in projection_model.port_connections
            if (pc.sender_role in ('plasticity', 'response') and
                pc.receiver_role in ('plasticity', 'response')))
        receive_conns = [pc for pc in projection_model.port_connections
                         if (pc.sender_role in ('pre', 'post') and
                             pc.receiver_role in ('plasticity', 'response'))]
        send_conns = [pc for pc in projection_model.port_connections
                      if (pc.sender_role in ('plasticity', 'response') and
                          pc.receiver_role in ('pre', 'post'))]
        syn_exps = chain(
            (BasePortExposure.from_port(pc.send_port,
                                        role2name[pc.sender_role])
             for pc in send_conns),
            (BasePortExposure.from_port(pc.receive_port,
                                        role2name[pc.receiver_role])
             for pc in receive_conns))
        synapse = MultiDynamicsProperties(
            name=(projection_model.name + '_syn'),
            sub_components=syn_comps,
            port_connections=syn_internal_conns,
            port_exposures=syn_exps)
        port_connections = list(chain(
            (pc.__class__(sender_role=pc.sender_role,
                          receiver_role='synapse',
                          send_port_name=pc.send_port_name,
                          receive_port_name=append_namespace(
                              pc.receive_port_name,
                              role2name[pc.receiver_role]))
             for pc in receive_conns),
            (pc.__class__(sender_role='synapse',
                          receiver_role=pc.receiver_role,
                          send_port_name=append_namespace(
                              pc.send_port_name,
                              role2name[pc.sender_role]),
                          receive_port_name=pc.receive_port_name)
             for pc in send_conns),
            (pc for pc in projection_model.port_connections
             if (pc.sender_role in ('pre', 'post') and
                 pc.receiver_role in ('pre', 'post')))))
        # A bit of a hack in order to bind the port_connections
        dummy_container = namedtuple('DummyContainer', 'pre post synapse')(
            projection_model.pre, projection_model.post, synapse)
        for port_connection in port_connections:
            port_connection.bind(dummy_container, to_roles=True)
        return synapse, port_connections

    @classmethod
    def _flatten_to_arrays_and_conns(cls, network_model):
        """
        Convert populations and projections into component arrays and
        connection groups
        """
        component_arrays = {}
        connection_groups = {}
        # Create flattened component with all synapses combined with the post-
        # synaptic cell dynamics using MultiDynamics
        for pop in network_model.populations:
            # Get all the projections that project to/from the given population
            receiving = [p for p in network_model.projections
                         if (pop == p.post or
                             (p.post.nineml_type == 'Selection' and
                              pop in p.post.populations))]
            sending = [p for p in network_model.projections
                       if (pop == p.pre or
                           (p.pre.nineml_type == 'Selection' and
                            pop in p.pre.populations))]
            # Create a dictionary to hold the cell dynamics and any synapse
            # dynamics that can be flattened into the cell dynamics
            # (i.e. linear ones).
            sub_components = {cls.CELL_COMP_NAME: pop.cell}
            # All port connections between post-synaptic cell and linear
            # synapses and port exposures to pre-synaptic cell
            internal_conns = []
            exposures = []

            def add_exposures(exposures_to_add):
                """
                Adds exposures to a "set" of exposures. If 9ML objects were
                hashable could use a 'set'.
                """
                for pe in exposures_to_add:
                    if pe not in exposures:
                        exposures.append(pe)

            synapses = []
            connection_property_sets = []
            # FIXME: There has to be a way of avoiding this name clash
            if any(p.name == cls.CELL_COMP_NAME for p in receiving):
                raise Pype9RuntimeError(
                    "Cannot handle projections named '{}' (why would you "
                    "choose such a silly name?;)".format(cls.CELL_COMP_NAME))
            for proj in receiving:
                # Flatten response and plasticity into single dynamics class.
                # TODO: this should be no longer necessary when we move to
                # version 2 as response and plasticity elements will be
                # replaced by a synapse element in the standard. It will need
                # be copied at this point though as it is modified
                synapse, proj_conns = cls._flatten_synapse(proj)
                # Get all connections to/from the pre-synaptic cell
                pre_conns = [pc for pc in proj_conns
                             if 'pre' in (pc.receiver_role, pc.sender_role)]
                # Get all connections between the synapse and the post-synaptic
                # cell
                post_conns = [pc for pc in proj_conns if pc not in pre_conns]
                # Mapping of port connection role to sub-component name
                role2name = {'post': cls.CELL_COMP_NAME}
                # If the synapse is non-linear it can be combined into the
                # dynamics of the post-synaptic cell.
                try:
                    if not synapse.component_class.is_linear():
                        raise Pype9UnflattenableSynapseException()
                    role2name['synapse'] = proj.name
                    # Extract "connection weights" (any non-singular property
                    # value) from the synapse properties
                    connection_property_sets.extend(
                        cls._extract_connection_property_sets(synapse,
                                                              proj.name))
                    # Add the flattened synapse to the multi-dynamics sub
                    # components
                    sub_components[proj.name] = synapse.clone()
                    # Convert port connections between synpase and post-
                    # synaptic cell into internal port connections of a multi-
                    # dynamics object
                    internal_conns.extend(pc.assign_names_from_roles(role2name)
                                          for pc in post_conns)
                    # Expose ports that are needed for the pre-synaptic
                    # connections
                except Pype9UnflattenableSynapseException:
                    # All synapses (of this type) connected to a single post-
                    # synaptic cell cannot be flattened into a single component
                    # of a multi- dynamics object so an individual synapses
                    # must be created for each connection.
                    synapse_conns = [
                        pc.append_namespace_from_roles(
                            {'post': cls.CELL_COMP_NAME,
                             'pre': cls.CELL_COMP_NAME,
                             'synapse': proj.name}) for pc in post_conns]
                    synapses.append(SynapseProperties(proj.name, synapse,
                                                      synapse_conns))
                    # Add exposures to the post-synaptic cell for connections
                    # from the synapse
                    add_exposures(chain(*(
                        pc.expose_ports({'post': cls.CELL_COMP_NAME})
                        for pc in post_conns)))
                # Add exposures for connections to/from the pre synaptic cell
                add_exposures(
                    chain(*(pc.expose_ports(role2name)
                            for pc in pre_conns)))
                role2name['pre'] = cls.CELL_COMP_NAME
            # Add exposures for connections to/from the pre-synaptic cell in
            # populations.
            for proj in sending:
                # Not required after transition to version 2 syntax
                synapse, proj_conns = cls._flatten_synapse(proj)
                # Add send and receive exposures to list
                add_exposures(chain(*(
                    pc.expose_ports({'pre': cls.CELL_COMP_NAME})
                    for pc in proj_conns)))
            # Add all cell ports as multi-component exposures that aren't
            # connected internally in case the user would like to save them or
            # play data into them
            internal_cell_ports = set(chain(
                (pc.send_port_name for pc in internal_conns
                 if pc.sender_name == cls.CELL_COMP_NAME),
                (pc.receive_port_name for pc in internal_conns
                 if pc.receiver_name == cls.CELL_COMP_NAME)))
            add_exposures(
                BasePortExposure.from_port(p, cls.CELL_COMP_NAME)
                for p in pop.cell.ports if p.name not in internal_cell_ports)
            dynamics_properties = MultiDynamicsProperties(
                name=pop.name + '_cell', sub_components=sub_components,
                port_connections=internal_conns,
                port_exposures=exposures)
            component = MultiDynamicsWithSynapsesProperties(
                dynamics_properties.name,
                dynamics_properties, synapse_propertiess=synapses,
                connection_property_sets=connection_property_sets)
            array_name = pop.name
            component_arrays[array_name] = ComponentArray9ML(
                array_name, pop.size, component)
        selections = {}
        for sel in network_model.selections:
            selections[sel.name] = Selection9ML(
                sel.name, Concatenate9ML(component_arrays[p.name]
                                           for p in sel.populations))
        arrays_and_selections = dict(
            chain(iter(component_arrays.items()), iter(selections.items())))
        # Create ConnectionGroups from each port connection in Projection
        for proj in network_model.projections:
            _, proj_conns = cls._flatten_synapse(proj)
            # Get all connections to/from the pre-synaptic cell
            pre_conns = [pc for pc in proj_conns
                         if 'pre' in (pc.receiver_role, pc.sender_role)]
            # Create a connection group for each port connection of the
            # projection to/from the pre-synaptic cell
            for port_conn in pre_conns:
                ConnectionGroupClass = (
                    EventConnectionGroup9ML
                    if port_conn.communicates == 'event'
                    else AnalogConnectionGroup9ML)
                if len(pre_conns) > 1:
                    name = ('__'.join((proj.name,
                                       port_conn.sender_role,
                                       port_conn.send_port_name,
                                       port_conn.receiver_role,
                                       port_conn.receive_port_name)))
                else:
                    name = proj.name
                if port_conn.sender_role == 'pre':
                    connectivity = proj.connectivity
                    # If a connection from the pre-synaptic cell the delay
                    # is included
                    # TODO: In version 2 all port-connections will have
                    # their own delays
                    delay = proj.delay
                else:
                    # If a "reverse connection" to the pre-synaptic cell
                    # the connectivity needs to be inverted
                    connectivity = InversePyNNConnectivity(
                        proj.connectivity)
                    delay = 0.0 * un.s
                # Append sub-component namespaces to the source/receive
                # ports
                ns_port_conn = port_conn.append_namespace_from_roles(
                    {'post': cls.CELL_COMP_NAME,
                     'pre': cls.CELL_COMP_NAME,
                     'synapse': proj.name})
                conn_group = ConnectionGroupClass(
                    name,
                    arrays_and_selections[proj.pre.name],
                    arrays_and_selections[proj.post.name],
                    source_port=ns_port_conn.send_port_name,
                    destination_port=(ns_port_conn.receive_port_name),
                    connectivity=connectivity,
                    delay=delay)
                connection_groups[conn_group.name] = conn_group
        return component_arrays, connection_groups, selections

    @classmethod
    def _extract_connection_property_sets(cls, dynamics_properties, namespace):
        """
        Identifies properties in the provided DynmaicsProperties that can be
        treated as a property of the connection (i.e. are not referenced
        anywhere except within the OnEvent blocks event port).
        """
        component_class = dynamics_properties.component_class
        varying_params = set(
            component_class.parameter(p.name).id
            for p in dynamics_properties.properties
            if p.value.nineml_type != 'SingleValue')
        # Get list of ports refereneced (either directly or indirectly) by
        # time derivatives and on-conditions
        not_permitted = set(p.id for p in component_class.required_for(
            chain(component_class.all_time_derivatives(),
                  component_class.all_on_conditions())).parameters)
        # If varying params intersects parameters that are referenced in time
        # derivatives they can not be redefined as connection parameters
        if varying_params & not_permitted:
            raise Pype9UnflattenableSynapseException()
        conn_params = defaultdict(dict)
        for on_event in component_class.all_on_events():
            for param in component_class.required_for(
                    on_event.state_assignments).parameters:
                if param.id in varying_params:
                    conn_params[on_event.src_port_name][param.id] = param
        return [
            ConnectionPropertySet(
                append_namespace(prt, namespace),
                [Property(append_namespace(p.name, namespace),
                          dynamics_properties.property(p.name).quantity)
                 for p in params.values()])
            for prt, params in conn_params.items()]

#             raise NotImplementedError(
#                 "Cannot convert population '{}' to component array as "
#                 "it has a non-linear synapse or multiple non-single "
#                 "properties")

#         # Get the properties, which are not single values, as they
#         # will have to be varied with each synapse. If there is
#         # only one it the weight of the synapse in NEURON and NEST
#         # can be used to hold it otherwise it won't be possible to
#         # collapse the synapses into a single dynamics object
#         non_single_props = [
#             p for p in synapse.properties
#             if not isinstance(p.value, SingleValue)]


class ComponentArray(object):
    """
    Component array object corresponds to a NineML type to be introduced in
    NineMLv2 (see https://github.com/INCF/nineml/issues/46), which consists of
    a dynamics class and a size. Populations and the synapses on incoming
    projections.

    Parameters
    ----------
    nineml_model : nineml.ComponentArray
        Component array nineml
    build_mode : str
        The build/compilation strategy for rebuilding the generated code, can
        be one of 'lazy', 'force', 'build_only', 'require'.
    """

    def __init__(self, nineml_model, build_mode='lazy', **kwargs):
        if not isinstance(nineml_model, ComponentArray9ML):
            raise Pype9RuntimeError(
                "Expected a component array, found {}".format(nineml_model))
        self._nineml = nineml_model
        dynamics_properties = nineml_model.dynamics_properties
        dynamics = dynamics_properties.component_class
        celltype = self.PyNNCellWrapperMetaClass(
            component_class=dynamics,
            default_properties=dynamics_properties,
            initial_state=list(dynamics_properties.initial_values),
            initial_regime=dynamics_properties.initial_regime,
            build_mode=build_mode, **kwargs)
        if build_mode != 'build_only':
            rng = self.Simulation.active().properties_rng
            cellparams = dict(
                (p.name, get_pyNN_value(p, self.UnitHandler, rng))
                for p in dynamics_properties.properties)
            initial_values = dict(
                (i.name, get_pyNN_value(i, self.UnitHandler, rng))
                for i in dynamics_properties.initial_values)
            initial_values['_regime'] = celltype.model.regime_index(
                dynamics_properties.initial_regime)
            # NB: Simulator-specific derived classes extend the corresponding
            # PyNN population class
            self.PyNNPopulationClass.__init__(
                self, nineml_model.size, celltype, cellparams=cellparams,
                initial_values=initial_values,
                label=nineml_model.name)
            self._inputs = {}
        self._t_stop = None
        self.Simulation.active().register_array(self)

    @property
    def name(self):
        return self._nineml.name

    @property
    def nineml(self):
        return self._nineml

    @property
    def component_class(self):
        return self.nineml.component_class

    def synapse(self, name):
        return self.nineml.dynamics_properties.synapse(name)

    def __repr__(self):
        return "ComponentArray('{}', size={})".format(self.name, self.size)

    def play(self, port_name, signal, properties=[]):
        """
        Plays an analog signal or train of events into a port of the dynamics
        array.

        Parameters
        ----------
        port_name : str
            The name of the port to play the signal into
        signal : neo.AnalogSignal | neo.SpikeTrain
            The signal to play into the cell
        properties : dict(str, nineml.Quantity)
            Connection properties when playing into a event receive port
            with static connection properties
        """
        port = self.celltype.model.component_class.receive_port(port_name)
        if port.nineml_type in ('EventReceivePort',
                                'EventReceivePortExposure'):
            # Shift the signal times to account for the minimum delay and
            # match the NEURON implementation
            try:
                spike_trains = Sequence(signal.rescale(pq.ms) -
                                        self._min_delay * pq.ms)
                source_size = 1
            except ValueError:  # Assume multiple signals
                spike_trains = []
                for spike_train in signal:
                    spike_train = (spike_train.rescale(pq.ms) -
                                   self._min_delay * pq.ms)
                    if any(spike_train <= 0.0):
                        raise Pype9RuntimeError(
                            "Some spike times are less than device delay ({}) "
                            "and so can't be played into cell ({})".format(
                                self._min_delay,
                                ', '.join(str(st) for st in spike_train[
                                    spike_train < self._min_delay])))
                    spike_trains.append(Sequence(spike_train))
                source_size = len(spike_trains)
            input_pop = self.PyNNPopulationClass(
                source_size, self.SpikeSourceArray,
                cellparams={'spike_times': spike_trains},
                label='{}-{}-input'.format(self.name, port_name))
#             self.celltype.model()._check_connection_properties(port_name,
#                                                                properties)
            if len(properties) > 1:
                raise NotImplementedError(
                    "Cannot handle more than one connection property per port")
            elif properties:
                weight = self.UnitHandler.scale_value(properties[0].quantity)
            else:
                weight = 1.0  # The weight var is not used
            connector = (self.OneToOneConnector()
                         if source_size > 1 else self.AllToAllConnector())
            input_proj = self.PyNNProjectionClass(
                input_pop, self, connector,
                self.SynapseClass(weight=weight, delay=self._min_delay),
                receptor_type=port_name,
                label='{}-{}-input_projection'.format(self.name, port_name))
            self._inputs[port_name] = (input_pop, input_proj)
        elif port.nineml_type in ('AnalogReceivePort', 'AnalogReducePort',
                                  'AnalogReceivePortExposure',
                                  'AnalogReducePortExposure'):
            raise NotImplementedError
#             # Signals are played into NEST cells include a delay (set to be the
#             # minimum), which is is subtracted from the start of the signal so
#             # that the effect of the signal aligns with other simulators
#             self._inputs[port_name] = nest.Create(
#                 'step_current_generator', 1,
#                 {'amplitude_values': pq.Quantity(signal, 'pA'),
#                  'amplitude_times': (
#                     signal.times.rescale(pq.ms) -
#                     controller.device_delay * pq.ms),
#                  'start': float(signal.t_start.rescale(pq.ms)),
#                  'stop': float(signal.t_stop.rescale(pq.ms))})
#             nest.Connect(self._inputs[port_name], self._cell,
#                          syn_spec={
#                              "receptor_type": self._receive_ports[port_name],
#                              'delay': controller.device_delay})
        else:
            raise Pype9RuntimeError(
                "Unrecognised port type '{}' to play signal into".format(port))

    def _get_port_details(self, port_name):
        """
        Return the communication type of the corresponding port and its fully
        qualified name in the cell-synapse namespace (e.g. the 'spike_output'
        port in the cell namespace will be 'spike_output__cell')

        Parameters
        ----------
        port_name : str
            Name of the port or state variable

        Returns
        -------
        communicates : str
            Either 'event' or 'analog' depending on the type of port port_name
            corresponds to
        record_name : str
            Name of the port fully qualified in the joint cell-synapse
            namespace
        """
        # TODO: Need to add a check that the port was recorded
        component_class = self.celltype.model.component_class
        port = None
        for name in (port_name, port_name + '__cell'):
            try:
                port = component_class.send_port(name)
            except NineMLNameError:
                try:
                    port = component_class.state_variable(name)
                except NineMLNameError:
                    pass
        if port is None:
            raise Pype9UsageError(
                "Unknown port or state-variable '{}' for '{}' "
                "component array (available '{}').".format(
                    port_name, self.name, "', '".join(chain(
                        component_class.send_port_names,
                        component_class.sub_component(
                            'cell').send_port_names))))
        if isinstance(port, StateVariable):
            communicates = 'analog'
        else:
            communicates = port.communicates
        return communicates, port.name

    def record(self, port_name, t_start=None):
        """
        Records the port or state variable

        Parameters
        ----------
        port_name : str
            Name of the port to record
        """

    def recording(self, port_name, t_start=None):
        """
        Returns the recorded data for the given port name

        Parameters
        ----------
        port_name : str
            The name of the port (or state-variable) to retrieve the recorded
            data for

        Returns
        -------
        recording : neo.Segment
            The recorded data in a neo.Segment
        """

        pyNN_data = self.get_data().segments[0]
        recording = neo.Segment()
        communicates, _ = self._get_port_details(port_name)
        if communicates == 'event':
            for st in pyNN_data.spiketrains:
                # FIXME: At some point we need to be able to specify multiple
                # event outputs
                if st.annotations:
                    if t_start is not None:
                        st = st[st > t_start]
                    recording.spiketrains.append(st)
        else:
            for asig in pyNN_data.analogsignals:
                # FIXME: Not sure if this will work
                if asig.annotations['name'] == port_name:
                    recording.analogsignals.append(asig)
        return recording

    def _kill(self, t_stop):
        """
        Caches all recording data and sets all references to the actual
        simulator object to None ahead of a simulator reset. This allows
        data to be accessed after a simulation has completed, and potentially
        a new simulation to have been started.
        """
        self._t_stop = t_stop

    @property
    def is_dead(self):
        return self._t_stop is None


class Selection(object):
    """
    A selection of cells from one or multiple component arrays. Used to
    connect ConnectionGroup.

    Parameters
    ----------
    nineml_model : nineml.Selection
        The NineML Selection object
    component_arrays : list(nineml.ComponentArray)
        List of component arrays included in the selection.
    """

    def __init__(self, nineml_model, *component_arrays):
        self._nineml = nineml_model
        self._component_arrays = dict(
            (ca.name, ca) for ca in component_arrays)
        try:
            assert(self._component_arrays)
        except:
            raise
        self.PyNNAssemblyClass.__init__(
            self, *component_arrays, label=nineml_model.name)

    @property
    def name(self):
        return self.nineml.name

    @property
    def nineml(self):
        return self._nineml

    @property
    def component_arrays(self):
        return iter(self._component_arrays.values())

    def component_array(self, name):
        return self._component_arrays[name]

    @property
    def num_component_arrays(self):
        return len(self._component_arrays)

    @property
    def component_array_names(self):
        return iter(self._component_arrays.keys())

    def synapse(self, name):
        try:
            synapses = set(
                ca.nineml.dynamics_properties.synapse_properties(name)
                for ca in self.component_arrays)
        except NineMLNameError:
            raise NineMLNameError(
                "Could not return synapse '{}' because it is missing from "
                "one or more of the component arrays in '{}' Selection"
                .format(name, self.name))
        if len(synapses) > 1:
            raise Pype9RuntimeError(
                "'{}' varies ({}) between component arrays in '{}' Selection"
                .format(name, ', '.join(str(s) for s in synapses), self.name))
        try:
            return next(iter(synapses))  # Return the only synapse
        except:
            raise

    def __repr__(self):
        return "Selection('{}', component_arrays=('{}')".format(
            self.name, "', '".join(self.component_array_names))


class ConnectionGroup(object):
    """
    ConnectionGroup object corresponds to a NineML type to be introduced in
    NineMLv2 (see https://github.com/INCF/nineml/issues/46), which consists of
    a dynamics class and a size. Only consists of references to ports on the
    source and destination ComponentArrays|Selections and connectivity.

    Parameters
    ----------
    nineml_model : nineml.ConnectionGroup
        Component array nineml
    source : ComponentArray
        Source component array
    destination : ComponentArray
        Destination component array
    """

    def __init__(self, nineml_model, source, destination):
        rng = self.Simulation.active().properties_rng
        if not isinstance(nineml_model, EventConnectionGroup9ML):
            raise Pype9RuntimeError(
                "Expected a connection group model, found {}"
                .format(nineml_model))
        try:
            (synapse, conns) = destination.synapse(nineml_model.name)
            if conns is not None:
                raise NotImplementedError(
                    "Nonlinear synapses, as used in '{}' are not currently "
                    "supported".format(nineml_model.name))
            if synapse.num_local_properties == 1:
                # Get the only local property that varies with the synapse
                # (typically the synaptic weight but does not have to be)
                weight = get_pyNN_value(next(synapse.local_properties),
                                        self.UnitHandler, rng)
            elif not synapse.num_local_properties:
                weight = 0.0
            else:
                raise NotImplementedError(
                    "Currently only supports one property that varies with "
                    "each synapse")
        except NineMLNameError:
            # FIXME: Should refactor "WithSynapses" code to "CellAndSynapses"
            #        class which inherits most of its functionality from
            #        MultiDynamics to ensure that every connection has a
            #        "synapse" even if it is just a simple port exposure
            # Synapse dynamics properties didn't have any properties that vary
            # between synapses so wasn't included
            weight = 0.0
        self._nineml = nineml_model
        delay = get_pyNN_value(nineml_model.delay, self.UnitHandler, rng)
        # FIXME: Ignores send_port, assumes there is only one...
        # NB: Simulator-specific derived classes extend the corresponding
        # PyNN population class
        self.PyNNProjectionClass.__init__(
            self,
            presynaptic_population=source,
            postsynaptic_population=destination,
            connector=nineml_model.connectivity,
            synapse_type=self.SynapseClass(weight=weight, delay=delay),
            receptor_type=nineml_model.destination_port,
            label=nineml_model.name)

    @property
    def name(self):
        return self._nineml.name

    @property
    def connectivity(self):
        return self._connector

    def __repr__(self):
        return ("ConnectionGroup('{}', source='{}', destination='{}', "
                "connectivity='{}')".format(self.name, self.pre.name,
                                            self.post.name,
                                            self.connectivity))
