"""
Module containing a point cell class

cell/pointCell.py 

Contains pointCell class 

Contributors: salvadordura@gmail.com, samnemo@gmail.com
"""
from __future__ import print_function
from __future__ import division
from __future__ import unicode_literals
from __future__ import absolute_import

from builtins import super
from builtins import zip
from builtins import range

from future import standard_library
standard_library.install_aliases()
from copy import deepcopy
from neuron import h # Import NEURON
import numpy as np
from .cell import Cell
from ..specs import Dict


###############################################################################
#
# POINT CELL CLASS (no compartments/sections)
#
###############################################################################

class PointCell (Cell):
    """
    Class for/to <short description of `netpyne.cell.pointCell.PointCell`>


    Point Neuron that doesn't use v from Section eg. NetStim, IntFire1, 
    """
    

    def __init__ (self, gid, tags, create=True, associateGid=True):
        from .. import sim

        # enable defining pointCell properties directly in cellParams
        exclude = ['cellType']

        #import IPython; IPython.embed()

        if 'cellType' in tags and tags['cellType'] in sim.net.params.cellParams:
            tags.update({k: v for k,v in sim.net.params.cellParams[tags['cellType']].items() if k not in exclude})

        # call parent calls (Cell) constructor to set gid and tags
        super(PointCell, self).__init__(gid, tags)
        self.hPointp = None
        if 'params' in self.tags:
            dictParams = sim.replaceDictODict(self.tags.pop('params'))
            self.params = deepcopy(dictParams)

        if create and sim.cfg.createNEURONObj:
            self.createNEURONObj()  # create cell
        if associateGid: self.associateGid() # register cell for this node

    def __str__ (self):
        try:
            gid, cmo = self.gid, self.tags['cellModel'] # only use if these exist
            return 'pointCell_%s_%d'%(cmo, gid)
        except: return 'pointCell%d'%self.gid

    def __repr__ (self):
        return self.__str__()

    def createNEURONObj (self):
        from .. import sim

        # add point processes
        try:
            self.hPointp = getattr(h, self.tags['cellModel'])()
        except:
            print("Error creating point process mechanism %s in cell with gid %d" % (self.tags['cellModel'], self.gid))
            return

        # if rate is list with 2 items generate random value from uniform
        if 'rate' in self.params and isinstance(self.params['rate'], list) and len(self.params['rate']) == 2:
            rand = h.Random()
            rand.Random123(sim.hashStr('point_rate'), self.gid, sim.cfg.seeds['stim']) # initialize randomizer
            self.params['rate'] = rand.uniform(self.params['rate'][0], self.params['rate'][1])


        # if rates is list with 2 items generate time-depenedent rates using vector.play()
        if 'rates' in self.params and isinstance(self.params['rates'], list) and len(self.params['rates']) == 2:
                interval = [1000.0/r if r>0.0 else 1e10 for r in self.params['rates'][0]]
                self.hVectorIntervals = h.Vector(interval)
                self.hVectorTimes = h.Vector(self.params['rates'][1])
                self.hVectorIntervals.play(self.hPointp._ref_interval, self.hVectorTimes)  # set continuous = True?

        # set pointp params - for PointCells these are stored in self.params
        params = {k: v for k,v in self.params.items()}
        for paramName, paramValue in params.items():
            try:
                if paramName == 'rate':
                    self.params['interval'] = 1000.0 / paramValue
                    setattr(self.hPointp, 'interval', self.params['interval'])
                else:
                    setattr(self.hPointp, paramName, paramValue)
            except:
                pass

        # add random num generator, and set number and seed for NetStims
        if self.tags['cellModel'] == 'NetStim':
            if sim.cfg.verbose: print("Creating a NetStim pointcell")
            rand = h.Random()
            self.hRandom = rand
            if 'number' not in self.params:
                params['number'] = 1e9
                setattr(self.hPointp, 'number', params['number'])
            if 'seed' not in self.params:
                self.params['seed'] = sim.cfg.seeds['stim'] # note: random number generator initialized from sim.preRun()


        # VecStim - generate spike vector based on params
        if self.tags['cellModel'] == 'VecStim':
            # seed
            if 'seed' not in self.params:
                self.params['seed'] = sim.cfg.seeds['stim']

            # convert rate to interval
            if 'rate' in self.params:
                self.params['interval'] = 1000.0/self.params['rate']

            # if interval
            if 'interval' in self.params:
                # set interval, start and noise params
                interval = self.params['interval']
                start = self.params['start'] if 'start' in self.params else 0.0
                noise = self.params['noise'] if 'noise' in self.params else 0.0

                maxReproducibleSpks = 1e4  # num of rand spikes generated; only a subset is used; ensures reproducibility

                # fixed interval of duration (1 - noise)*interval
                fixedInterval = np.full(int(((1+1.5*noise)*sim.cfg.duration/interval)), [(1.0-noise)*interval])  # generate 1+1.5*noise spikes to account for noise
                numSpks = len(fixedInterval)

                # randomize the first spike so on average it occurs at start + noise*interval
                # invl = (1. - noise)*mean + noise*mean*erand() - interval*(1. - noise)
                if noise == 0.0:
                    vec = h.Vector(len(fixedInterval))
                    spkTimes =  np.cumsum(fixedInterval) + (start - interval)
                else:
                    # plus negexp interval of mean duration noise*interval. Note that the most likely negexp interval has duration 0.
                    rand = h.Random()
                    rand.Random123(sim.hashStr('vecstim_spkt'), self.gid, self.params['seed'])

                    # Method 1: vec length depends on duration -- not reproducible
                    # vec = h.Vector(numSpks)
                    # rand.negexp(noise*interval)
                    # vec.setrand(rand)
                    # negexpInterval= np.array(vec)
                    # #print negexpInterval
                    # spkTimes = np.cumsum(fixedInterval + negexpInterval) + (start - interval*(1-noise))

                    if numSpks < 100:
                        # Method 2: vec length=1, slower but reproducible
                        vec = h.Vector(1)
                        rand.negexp(noise*interval)
                        negexpInterval = []
                        for i in range(numSpks):
                            vec.setrand(rand)
                            negexpInterval.append(vec.x[0])  # = np.array(vec)[0:len(fixedInterval)]
                        spkTimes = np.cumsum(fixedInterval + np.array(negexpInterval)) + (start - interval*(1-noise))

                    elif numSpks < maxReproducibleSpks:
                        # Method 3: vec length=maxReproducibleSpks, then select subset; slower but reproducible
                        vec = h.Vector(maxReproducibleSpks)
                        rand.negexp(noise*interval)
                        vec.setrand(rand)
                        negexpInterval = np.array(vec.c(0,len(fixedInterval)-1))
                        spkTimes = np.cumsum(fixedInterval + negexpInterval) + (start - interval*(1-noise))

                    else:
                        print('\nError: exceeded the maximum number of VecStim spikes per cell (%d > %d)' % (numSpks, maxReproducibleSpks))
                        return

            # spikePattern
            elif 'spikePattern' in self.params:
                patternType = self.params['spikePattern'].get('type', None)
                rand = h.Random()

                if self.params['spikePattern'].get('sync', False):
                    rand.Random123(sim.hashStr('vecstim_spikePattern'), self.params['seed'])
                else:
                    rand.Random123(sim.hashStr('vecstim_spikePattern'), self.gid, self.params['seed'])

                if patternType == 'rhythmic':
                    from .inputs import createRhythmicPattern
                    spkTimes = createRhythmicPattern(self.params['spikePattern'], rand)
                elif patternType == 'evoked':
                    from .inputs import createEvokedPattern
                    spkTimes = createEvokedPattern(self.params['spikePattern'], rand)
                elif patternType == 'poisson':
                    from .inputs import createPoissonPattern
                    if self.params['spikePattern']['stop'] == -1:
                        self.params['spikePattern']['stop'] = sim.cfg.duration
                    spkTimes = createPoissonPattern(self.params['spikePattern'], rand)
                elif patternType == 'gauss':
                    from .inputs import createGaussPattern
                    spkTimes = createGaussPattern(self.params['spikePattern'], rand)
                else:
                    print('\nError: invalid spikePattern type %s' % (patternType))
                    return

                vec = h.Vector(len(spkTimes))

            # if spkTimess
            elif 'spkTimes' in self.params:
                spkTimes = self.params['spkTimes']
                if type(spkTimes) not in (list,tuple,np.array):
                    print('\nError: VecStim "spkTimes" needs to be a list, tuple or numpy array')
                    return
                spkTimes = np.array(spkTimes)
                vec = h.Vector(len(spkTimes))

            # if spkTimess
            elif 'spkTimes' in self.tags:
                spkTimes = self.tags['spkTimes']
                if type(spkTimes) not in (list,tuple,np.array):
                    print('\nError: VecStim "spkTimes" needs to be a list, tuple or numpy array')
                    return
                spkTimes = np.array(spkTimes)
                vec = h.Vector(len(spkTimes))

            # missing params
            else:
                print('\nError: VecStim requires interval, rate or spkTimes')
                return

            # pulse list: start, end, rate, noise
            if 'pulses' in self.params:
                for ipulse, pulse in enumerate(self.params['pulses']):

                    # if rate is list with 2 items generate random value from uniform
                    if 'rate' in pulse and isinstance(pulse['rate'], list) and len(pulse['rate']) == 2:
                        rand = h.Random()
                        rand.Random123(sim.hashStr('point_rate_pulse'+str(ipulse)), self.gid, sim.cfg.seeds['stim']) # initialize randomizer
                        pulse['rate'] = rand.uniform(pulse['rate'][0], pulse['rate'][1])

                    # check interval or rate params
                    if 'interval' in pulse:
                        interval = pulse['interval']
                    elif 'rate' in pulse:
                        interval = 1000.0/pulse['rate']
                    else:
                        print('Error: Vecstim pulse missing "rate" or "interval" parameter')
                        return

                    # check start,end and noise params
                    if any([x not in pulse for x in ['start', 'end']]):
                        print('Error: Vecstim pulse missing "start" and/or "end" parameter')
                        return
                    else:
                        noise = pulse['noise'] if 'noise' in pulse else 0.0
                        start = pulse['start']
                        end = pulse['end']

                        # fixed interval of duration (1 - noise)*interval
                        fixedInterval = np.full(int(((1+1.5*noise)*(end-start)/interval)), [(1.0-noise)*interval])  # generate 1+0.5*noise spikes to account for noise
                        numSpks = len(fixedInterval)

                        # randomize the first spike so on average it occurs at start + noise*interval
                        # invl = (1. - noise)*mean + noise*mean*erand() - interval*(1. - noise)
                        if noise == 0.0:
                            vec = h.Vector(len(fixedInterval))
                            pulseSpikes = np.cumsum(fixedInterval) + (start - interval)
                            pulseSpikes[pulseSpikes < start] = start
                            spkTimes = np.append(spkTimes, pulseSpikes[pulseSpikes <= end])
                        else:
                            # plus negexp interval of mean duration noise*interval. Note that the most likely negexp interval has duration 0.
                            rand = h.Random()