"""Synapse Stimuli."""

# Copyright 2020-2022 Blue Brain Project / EPFL

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import random
from bluepyopt import ephys


class NrnNetStimStimulusCustom(ephys.stimuli.Stimulus):
    """Current stimulus based on current amplitude and time series.

    Attributes:
        total_duration (float): duration of run (ms)
        locations (list): synapse point processes locations to connect to
        interval (float): time between spikes (ms)
        number (int): average number of spikes
        start (float): most likely start time of first spike (ms)
        noise (float): fractional randomness (0 deterministic,
                1 negexp interval distrubtion)
        connections (dict): contains simulator NetCon and NetStim
            so that they are persistent
    """

    def __init__(
        self,
        locations=None,
        total_duration=None,
        interval=None,
        number=None,
        start=None,
        noise=0,
    ):
        """Constructor.

        Args:
            locations (list): synapse point processes locations to connect to
            total_duration (float): duration of run (ms)
            interval (float): time between spikes (ms)
            number (int): average number of spikes
            start (float): most likely start time of first spike (ms)
            noise (float): fractional randomness (0 deterministic,
                   1 negexp interval distrubtion)
        """
        super().__init__()
        if total_duration is None:
            raise ValueError("NrnNetStimStimulus: Need to specify a total duration")
        self.total_duration = total_duration

        self.locations = locations
        self.interval = interval
        self.number = number
        self.start = start
        self.noise = noise
        self.connections = {}

    def instantiate(self, sim=None, icell=None):
        """Instantiate stimuli and connections.

        Args:
            sim (bluepyopt.ephys.NrnSimulator): neuron simulator
            icell (neuron cell): cell instantiation in simulator
        """
        if self.connections is None:
            self.connections = {}
        for location in self.locations:
            self.connections[location.name] = []
            for synapse in location.instantiate(sim=sim, icell=icell):
                netstim = sim.neuron.h.NetStim()
                netstim.interval = (
                    synapse.interval if synapse.interval is not None else self.interval
                )
                netstim.number = (
                    synapse.number if synapse.number is not None else self.number
                )
                netstim.start = (
                    synapse.start if synapse.start is not None else self.start
                )
                netstim.noise = (
                    synapse.noise if synapse.noise is not None else self.noise
                )
                netcon = sim.neuron.h.NetCon(
                    netstim, synapse.hsynapse, -30, synapse.delay, synapse.weight
                )

                self.connections[location.name].append((netcon, netstim))

    def destroy(self, sim=None):
        """Destroy stimulus.

        Args:
            sim (bluepyopt.ephys.NrnSimulator): neuron simulator
        """
        # pylint: disable=unused-argument
        self.connections = None

    def __str__(self):
        """String representation."""
        # pylint: disable=consider-using-f-string
        return (
            "Netstim at %s" % ",".join(location for location in self.locations)
            if self.locations is not None
            else "Netstim"
        )


class NrnVecStimStimulusCustom(ephys.stimuli.Stimulus):
    """Current stimulus based on stochastic current amplitude.

    Attributes:
        total_duration (float): time after which no synapses are allowed to fire (ms)
        locations (list): synapse point processes locations to connect to
        start (float): most likely start time of first spike (ms)
        seed (int): seed for random number generator
        vecstim_random (str): origin of the random nmb gener. can be "python" or "neuron"
        pre_spike_train (list): list of spike train. If None, will be generated by random numbers
        connections (dict): contains simulator NetCon and VecStim and time Vector
            so that they are persistent
    """

    def __init__(
        self,
        locations=None,
        start=None,
        stop=None,
        seed=1,
        vecstim_random="python",
        pre_spike_train=None,
    ):
        """Constructor.

        Args:
            locations (list): synapse point processes locations to connect to
            start (float): most likely start time of first spike (ms)
            stop (float): time after which no synapses are allowed to fire (ms)
            seed (int): seed for random number generator
            vecstim_random (str): origin of the random nmb gener. can be "python" or "neuron"
            pre_spike_train (list): list of spike train.
                If None, will be generated by random numbers
        """
        super().__init__()
        if stop is None:
            raise ValueError("NrnVecStimStimulus: Need to specify a stop time")
        # must be named total_duration because of ephys.protocols
        self.total_duration = stop

        self.locations = locations
        self.start = start
        self.seed = seed
        self.vecstim_random = vecstim_random
        self.pre_spike_train = pre_spike_train
        self.connections = {}

    def instantiate(self, sim=None, icell=None):
        """Instantiate stimuli and connections.

        Args:
            sim (bluepyopt.ephys.NrnSimulator): neuron simulator
            icell (neuron cell): cell instantiation in simulator
        """
        if self.connections is None:
            self.connections = {}

        if self.pre_spike_train is None:
            if self.vecstim_random == "python":
                random.seed(self.seed)
            else:
                rand = sim.neuron.h.Random(self.seed)
                rand.uniform(self.start, self.total_duration)

        for location in self.locations:
            self.connections[location.name] = []
            for synapse in location.instantiate(sim=sim, icell=icell):
                # create spike_train
                if self.pre_spike_train is not None:
                    spike_train = self.pre_spike_train
                else:
                    if self.vecstim_random == "python":
                        spike_train = [random.uniform(self.start, self.total_duration)]
                    else:
                        spike_train = [rand.repick()]

                t_vec = sim.neuron.h.Vector(spike_train)
                vecstim = sim.neuron.h.VecStim()
                vecstim.play(t_vec, sim.dt)
                netcon = sim.neuron.h.NetCon(
                    vecstim, synapse.hsynapse, -30, synapse.delay, synapse.weight
                )

                self.connections[location.name].append((netcon, vecstim, t_vec))

    def destroy(self, sim=None):
        """Destroy stimulus.

        Args:
            sim (bluepyopt.ephys.NrnSimulator): neuron simulator
        """
        # pylint: disable=unused-argument
        self.connections = None

    def __str__(self):
        """String representation."""
        # pylint: disable=consider-using-f-string
        return (
            "Vecstim at %s" % ",".join(location for location in self.locations)
            if self.locations is not None
            else "Vecstim"
        )


class NetConSpikeDetector(ephys.stimuli.Stimulus):
    """Netcon linking output from pre-cell to post-cell's synapses.

    Attributes:
        total_duration (float): end time of connection (ms)
        locations (list): synapse point processes locations to connect to
        connections (dict): contains simulator NetCon so that they are persistent
    """

    def __init__(self, total_duration=None, locations=None):
        """Constructor.

        Args:
            total_duration (float): end time of connection (ms)
            locations (list): synapse point processes locations to connect to
        """
        self.total_duration = total_duration
        self.locations = locations

        self.connections = {}

    def instantiate(self, sim, icell):
        """Instantiate connections.

        Args:
            sim (bluepyopt.ephys.NrnSimulator): neuron simulator
            icell (neuron cell): cell instantiation in simulator
        """
        if self.connections is None:
            self.connections = {}

        for location in self.locations:
            self.connections[location.name] = []
            for synapse in location.instantiate(sim=sim, icell=icell):
                # taken from bglibpy.cell.create_netcon_spikedetector
                # M. Hines magic to return a variable by reference to a python function
                netcon = sim.neuron.h.ref(None)
                icell.getCell().connect2target(synapse.hsynapse, netcon)
                netcon = netcon[0]
                netcon.weight[0] = synapse.weight
                netcon.delay = synapse.delay

                self.connections[location.name].append(netcon)

    def destroy(self, sim=None):
        """Destroy stimulus.

        Args:
            sim (bluepyopt.ephys.NrnSimulator): neuron simulator
        """
        # pylint: disable=unused-argument
        self.connections = None

    def __str__(self):
        """String representation."""
        # pylint: disable=consider-using-f-string
        return (
            "NetconSpikeDetector at %s"
            % ",".join(location for location in self.locations)
            if self.locations is not None
            else "NetconSpikeDetector"
        )
