/**
* "FNS" (Firnet NeuroScience), ver.3.x
*				
* FNS is an event-driven Spiking Neural Network framework, oriented 
* to data-driven neural simulations.
*
* (c) 2020, Gianluca Susi, Emanuele Paracone, Mario Salerno, 
* Alessandro Cristini, Fernando Maestú.
*
* CITATION:
* When using FNS for scientific publications, cite us as follows:
*
* Gianluca Susi, Pilar Garcés, Alessandro Cristini, Emanuele Paracone, 
* Mario Salerno, Fernando Maestú, Ernesto Pereda (2020). 
* "FNS: an event-driven spiking neural network simulator based on the 
* LIFL neuron model". 
* Laboratory of Cognitive and Computational Neuroscience, UPM-UCM 
* Centre for Biomedical Technology, Technical University of Madrid; 
* University of Rome "Tor Vergata".   
* Paper under review.
*
* FNS is free software: you can redistribute it and/or modify it 
* under the terms of the GNU General Public License version 3 as 
* published by the Free Software Foundation.
*
* FNS is distributed in the hope that it will be useful, but WITHOUT 
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
* or FITNESS FOR A PARTICULAR PURPOSE. 
* See the GNU General Public License for more details.
* 
* You should have received a copy of the GNU General Public License 
* along with FNS. If not, see <http://www.gnu.org/licenses/>.
* 
* -----------------------------------------------------------
*  
* Website:   http://www.fnsneuralsimulator.org
* 
* Contacts:  fnsneuralsimulator (at) gmail.com
*	    gianluca.susi82 (at) gmail.com
*	    emanuele.paracone (at) gmail.com
*
*
* -----------------------------------------------------------
* -----------------------------------------------------------
**/

package spiking.controllers.node;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.PriorityQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import spiking.internode.InterNodeBurningSpike;
import spiking.internode.InterNodeSpike;
import spiking.node.Node;
import spiking.node.NodesManager;
import spiking.node.Synapse;
import spiking.node.SynapsesManager;
import spiking.node.neuron.NodeNeuronsManager;
import spiking.node.spikes.FixedBurnSpike;
import utils.constants.Constants;
import utils.tools.NiceNode;
import utils.tools.NiceQueue;
import utils.math.FastMath;
import utils.statistics.StatisticsCollector;
import utils.statistics.BurningWriter;
import utils.statistics.FiringWriter;

public class NodeThread extends Thread{
  
  private final static String TAG = "[Node Thread ";
  private final static Boolean verbose = true;

  private Node n;
  private NodeNeuronsManager nnMan;
  private NodesManager nMan;
  private SynapsesManager synMan;
  
  private Boolean plasticity;
  //queues map enqueues the spikes sended by a specific firing neuron 
  //to a specific burning neuron
  //extendible
  private HashMap<Synapse, NiceQueue>queuesMap;
  private double currentTime=0.0;
  private double stopTime=0.0;
  private double startTime=0.0;
  private Boolean debug = false;
  private Boolean activePassiveDebug = false;
  private int debug_level = 3;
  private int debNum=28;
  private ArrayList<InterNodeSpike> internodeFires;  
  private Boolean keepRunning=true;
  private Lock lock = new ReentrantLock();
  private Condition newTimeSplitCond = lock.newCondition();
  private PriorityQueue<InterNodeBurningSpike> interNodeBurningSpikes=
      new PriorityQueue<InterNodeBurningSpike>();
  private PriorityQueue<FixedBurnSpike> burningQueueSpikes=
      new PriorityQueue<FixedBurnSpike>();
  private FastMath fm = new FastMath();
  private Double debugMaxWPDiff=0.0;
  private Boolean do_fast;
  private Boolean isNOI;
  private Boolean lif = false;
  private Boolean exp_decay = false;
  long[] times= new long[10];
  private StatisticsCollector sc;
  private int toremDebug=0;
  private BurningWriter burningWriter;
  private FiringWriter firingWriter;
  //private NodeFiringsThread nft;
  //private NodeBurningsThread nbt;
  

  /**
  *   The NodeThread object
  *
  *   @param nMan             the simulation NodesManager object
  *   @param id               the node id
  *   @param n                the number of neurons of the node
  *   @param excitProportion  the ratio of excitatory neurons over the total
  *                           number of neurons 'n'
  *   @param k                the conn-degree of each neuron
  *   @param prew             the prob of small-world topology rewinig
  *   @param Bn               the number of bursts spike for each 
  *                           firing neuron
  *   @param IBI              the burst inter-spike time 
  *   @param D_exc
  *   @param D_inh
  *   @param mu_w_exc         the mean of the postsynaptic weight 
  *                           distribution for excitatory neurons
  *   @param mu_w_inh         the mean of the postsynaptic weight 
  *                           distribution for inhibitory neurons
  *   @param sigma_w_exc      the std deviation of the postsynaptic weight
  *                           distribution for excitatory neurons
  *   @param sigma_w_inh      the std deviation of the postsynaptic weight
  *                           distribution for inhibitory neurons
  *   @param w_pre_exc        the presynaptic weight for excitatory neurons
  *   @param w_pre_inh        the presynaptic weight for inhibitory neurons
  *   @param externalPresynapticDefVal
  *   @param plasticity       simulate neuron plasticity
  *   @param etap             the Etap for plasticity
  *   @param etam             the Etam for plasticity
  *   @param taup             the Taup for plasticity
  *   @param taum             the Taum for plasticity
  *   @param pwMax            the pwMax for plasticity
  *   @param to               the to for plasticity
  *   @param avgNeuronalSignalSpeed the signal speed through synapsis
  *   @param lif              do lif simulation
  *   @param exp_decay        use exponential sub-threashold decay
  *   @param do_fast          use fastest algorithms (some aproximations)
  *
  */
  public NodeThread(
      NodesManager nMan, 
      Integer id, 
      Long n, 
      Double excitProportion,
      Integer k, 
      Double prew,
      Integer Bn,
      Double IBI,      
      Double D_exc, 
      Double D_inh, 
      Double ld, 
      Double kr,
      Double mu_w_exc,
      Double mu_w_inh,
      Double sigma_w_exc,
      Double sigma_w_inh,
      Double w_pre_exc,
      Double w_pre_inh,
      Double externalPresynapticDefVal, 
      Boolean plasticity,
      Double etap,
      Double etam,
      Double taup,
      Double taum, 
      Double pwMax,
      Double to,
      Double avgNeuronalSignalSpeed,
      Boolean lif,
      Boolean exp_decay,
      Boolean do_fast,
      Boolean isNOI,
      StatisticsCollector sc){
    this.n=new Node(
        id,
        n, 
        excitProportion,
        mu_w_exc,
        mu_w_inh,
        sigma_w_exc,
        sigma_w_inh,
        w_pre_exc,
        w_pre_inh,
        k, 
        prew,
        Bn,
        IBI,
        plasticity,
        etap,
        etam,
        taup,
        taum,
        pwMax,
        to);
    init(nMan, 
        D_exc, 
        D_inh, 
        ld, 
        kr, 
        w_pre_exc, 
        w_pre_inh, 
        externalPresynapticDefVal, 
        avgNeuronalSignalSpeed,
        lif,
        exp_decay,
        do_fast,
        isNOI,
        sc);
  }
  
  /**
  *   The NodeThread object
  *
  *   @param nMan             the simulation NodesManager object
  *   @param id               the node id
  *   @param n                the number of neurons of the node
  *   @param externalInputs   the number of neurons with external inputs
  *   @param externalInputType the type of external input 
  *                            0. poisson
  *                            1. constant
  *                            2.noise
  *   @param externalInputsTimeOffset the time offset before external input  
  *                           firings to begin
  *   @param timeStep         the avg for the external input fire distribution
  *   @param fireDuration     the duration of external input activity
  *   @param externalAmplitude the amplitude of an external input signal
  *   @param externalOutdegree the external out-degree of each external input
  *   @param excitRatio  the ratio of excitatory neurons over the total
  *                           number of neurons 'n'
  *   @param k                the conn-degree of each neuron
  *   @param prew             the prob of small-world topology rewinig
  *   @param Bn               the number of bursts spike for each 
  *                           firing neuron
  *   @param IBI              the burst inter-spike time 
  *   @param D_exc
  *   @param D_inh
  *   @param mu_w_exc         the mean of the postsynaptic weight 
  *                           distribution for excitatory neurons
  *   @param mu_w_inh         the mean of the postsynaptic weight 
  *                           distribution for inhibitory neurons
  *   @param sigma_w_exc      the std deviation of the postsynaptic weight
  *                           distribution for excitatory neurons
  *   @param sigma_w_inh      the std deviation of the postsynaptic weight
  *                           distribution for inhibitory neurons
  *   @param w_pre_exc        the presynaptic weight for excitatory neurons
  *   @param w_pre_inh        the presynaptic weight for inhibitory neurons
  *   @param externalPresynapticDefVal
  *   @param plasticity       simulate neuron plasticity
  *   @param etap             the Etap for plasticity
  *   @param etam             the Etam for plasticity
  *   @param taup             the Taup for plasticity
  *   @param taum             the Taum for plasticity
  *   @param pwMax            the pwMax for plasticity
  *   @param to               the to for plasticity
  *   @param avgNeuronalSignalSpeed the signal speed through synapsis
  *   @param lif              do lif simulation
  *   @param exp_decay        use exponential sub-threashold decay
  *   @param do_fast          use fastest algorithms (some aproximations)
  *
  */
  public NodeThread(
      NodesManager nMan, 
      Integer id, 
      Long n, 
      Integer externalInputs, 
      int externalInputType,
      Double externalInputsTimeOffset,
      double timeStep, 
      //Double fireRate,  
      int fireDuration,
      Double externalAmplitude,
      int externalOutdegree,
      Double excitRatio,
      Integer k, 
      Double prew, 
      Integer Bn,
      Double IBI,
      Double c, 
      Double D_exc, 
      Double D_inh, 
      Double t_arp,
      Double mu_w_exc,
      Double mu_w_inh,
      Double sigma_w_exc,
      Double sigma_w_inh,
      Double w_pre_exc,
      Double w_pre_inh,
      Double externalPresynapticDefVal, 
      Boolean plasticity,
      Double etap,
      Double etam,
      Double taup,
      Double taum, 
      Double pwMax,
      Double to,
      Double avgNeuronalSignalSpeed,
      Boolean lif, 
      Boolean exp_decay, 
      Boolean do_fast,
      Boolean isNOI,
      StatisticsCollector sc){
    this.n=new Node(id,
        n,
        externalInputs,
        externalInputType,
        externalInputsTimeOffset,
        timeStep,
        //fireRate, 
        fireDuration,
        externalAmplitude, 
        externalOutdegree,
        excitRatio,
        mu_w_exc,
        mu_w_inh,
        sigma_w_exc,
        sigma_w_inh,
        w_pre_exc,
        w_pre_inh,
        k, 
        prew, 
        Bn,
        IBI,
        plasticity,
        etap,
        etam,
        taup,
        taum,
        pwMax,
        to);
    init(nMan, 
        c, 
        D_exc, 
        D_inh, 
        t_arp, 
        w_pre_exc, 
        w_pre_inh, 
        externalPresynapticDefVal, 
        avgNeuronalSignalSpeed,
        lif, 
        exp_decay, 
        do_fast,
        isNOI,
        sc);
    
  }

  
  /**
   *   The initialization function for NodeThread
   *   @param nMan                          the NodesManager
   *   @param c                               
   *   @param D_exc                          
   *   @param D_inh 
   *   @param t_arp 
   *   @param excitatoryPresynapticDefVal   the presynaptic weight for 
   *                                        excitatory neurons
   *   @param inhibithoryPresynapticDefVal  the presynaptic weight for 
   *                                        inhibitory neurons
   *   @param externalPresynapticDefVal     the presynaptic weight for 
   *                                        external inputs 
   *   @param avgNeuronalSignalSpeed        the avg signal through axons
   *   @param lif                           if true, a least integrate 
   *                                        and fire simulation is performed
   *   @param exp_decay                     if true, the underthreashold 
   *                                        neuronal decay would be 
   *                                        exponential
   *   @param do_fast                       if true, fastest algorithm 
   *                                        are used for expensive 
   *                                        calculations. This  introduces 
   *                                        some kind of aproximations
   */
  public void init(
      NodesManager nMan, 
      Double c, 
      Double D_exc, 
      Double D_inh, 
      Double t_arp, 
      Double excitatoryPresynapticDefVal, 
      Double inhibithoryPresynapticDefVal, 
      Double externalPresynapticDefVal, 
      Double avgNeuronalSignalSpeed,
      Boolean lif, 
      Boolean exp_decay, 
      Boolean do_fast,
      Boolean isNOI,
      StatisticsCollector sc){
    this.sc=sc;
    //nft=new NodeFiringsThread(this.sc);
    //nbt=new NodeBurningsThread(this.sc);
    //nft.start();
    //nbt.start();
    queuesMap = new HashMap<Synapse, NiceQueue>();
    internodeFires = new ArrayList<InterNodeSpike>();
    nnMan = new NodeNeuronsManager(n, 
        c, 
        D_exc, 
        D_inh, 
        t_arp, 
        excitatoryPresynapticDefVal, 
        inhibithoryPresynapticDefVal, 
        externalPresynapticDefVal);
    this.plasticity=n.getPlasticity();
    initExtInput();
    synMan = new SynapsesManager(n,avgNeuronalSignalSpeed);
    this.nMan=nMan;
    println("Bn: "+n.getBn());
    println("IBI: "+n.getIBI());
    println("c: "+c);
    println("D exc: "+D_exc);
    println("D inh: "+D_inh);
    println("t arp: "+t_arp);
    this.lif=lif;
    this.exp_decay=exp_decay;
    this.do_fast=do_fast;
    this.isNOI=isNOI;
  }
  
  /**
   *  starts the main thread routine
   */
  public void run(){
    NiceNode minFiringNeuron;
    Long firingNeuronId=null;
    Double spikeTime=null;
    Double tmpMinFiringTime;
    Double tmpMinInterNodeBurningTime;
    Double minFixedBurnTime;
    InterNodeBurningSpike tmpInterNodeBurningSpike;
    FixedBurnSpike tmpFixedBurnSpike;
    Integer lastCollectedBurstFiringNodeId=-1;
    Long lastCollectedBurstFiringNeuronId=-1l;
    Double lastCollectedBurstFiringBurnTime=-1.0;
    int fires=0;
    Boolean stopped=false;  
    //the actual thread routine
    while (keepRunning){
      for (; currentTime<stopTime; ++fires){
        if (!stopped){
          /* check which is the minimum between the
           * next firing time, the next burn due to inter-node
           * spikes and the next burn due to bursting queue
           * of a fire already happened
           */
          tmpMinFiringTime=nnMan.getMinFiringTime();
          tmpInterNodeBurningSpike=interNodeBurningSpikes.peek();
          tmpFixedBurnSpike=burningQueueSpikes.peek();
          minFixedBurnTime=(tmpFixedBurnSpike==null)?
              Double.MAX_VALUE:
                tmpFixedBurnSpike.getBurnTime();
          // case of first arrival of inter-node burn  
          if (tmpInterNodeBurningSpike!=null){
            tmpMinInterNodeBurningTime = 
                tmpInterNodeBurningSpike.getTimeToBurn();
            if ((tmpMinInterNodeBurningTime!=null)&&
                (tmpMinInterNodeBurningTime<stopTime)){
              if (tmpMinFiringTime!=null){
                // inter-node burn processing first
                if ((tmpMinInterNodeBurningTime<tmpMinFiringTime) &&
                    (tmpMinInterNodeBurningTime<minFixedBurnTime)){
                  InterNodeSpike irs=
                      interNodeBurningSpikes.poll().getInterNodeSpike();
                  if (tmpMinInterNodeBurningTime<currentTime) {
                    println("internode burning:"+
                        tmpMinInterNodeBurningTime+
                        " min FixedBurn:"+
                        minFixedBurnTime+
                        " tmpMinFiringTime:"+
                        tmpMinFiringTime);
                    if (interNodeBurningSpikes.peek()!=null)
                      println("polled:"+
                          irs.getBurnTime()+
                          " peeked:"+
                          interNodeBurningSpikes.peek().getTimeToBurn()+
                          "\n");
                  }
                  if ((interNodeBurningSpikes.peek()!=null)&&
                      (irs.getBurnTime()>interNodeBurningSpikes.peek().getTimeToBurn()))
                    println("polled:"+
                        irs.getBurnTime()+
                        " peeked:"+
                        interNodeBurningSpikes.peek().getTimeToBurn()+
                        " syn:"+
                        tmpInterNodeBurningSpike.getInterNodeSpike().getSyn());
                  currentTime=tmpMinInterNodeBurningTime;
                  burnNeuron(
                      irs.getSyn(), 
                      irs.getBurnTime(), 
                      irs.getFireTime(), 
                      false);
                  continue;
                }
              }
              // there is no next node-internal spike, check inter-node 
              // against fixed burn 
              else if (tmpMinInterNodeBurningTime<minFixedBurnTime){
                InterNodeSpike irs=
                    interNodeBurningSpikes.poll().getInterNodeSpike();
                currentTime=tmpMinInterNodeBurningTime;
                burnNeuron(
                    irs.getSyn(), 
                    irs.getBurnTime(), 
                    irs.getFireTime(), 
                    false);
                continue;
              }
            }
          }
          // case of first arrival of bursting queue spike to be burn
          if (minFixedBurnTime<stopTime) {
            if (tmpMinFiringTime!=null){
              if (minFixedBurnTime<tmpMinFiringTime) {
                FixedBurnSpike fixedBurnSpike = burningQueueSpikes.poll();
                if (tmpFixedBurnSpike.getBurnTime()!=
                    fixedBurnSpike.getBurnTime()) {
                  println("tada!:"+
                      tmpFixedBurnSpike.getBurnTime()+
                      "!="+
                      fixedBurnSpike.getBurnTime());
                  System.exit(1);
                }
                currentTime=minFixedBurnTime;
                burnNeuron(
                    fixedBurnSpike.getSyn(),
                    fixedBurnSpike.getBurnTime(),
                    minFixedBurnTime,
                    false);
                if(
                    (! lastCollectedBurstFiringNodeId.equals(n.getId()))||
                    (! lastCollectedBurstFiringNeuronId.equals(
                        fixedBurnSpike.getSyn().getAxonNeuronId()))||
                    (! lastCollectedBurstFiringBurnTime.equals(
                        fixedBurnSpike.getBurnTime()))
                ){
                  lastCollectedBurstFiringNodeId=n.getId();
                  lastCollectedBurstFiringNeuronId=
                      fixedBurnSpike.getSyn().getAxonNeuronId();
                  lastCollectedBurstFiringBurnTime=
                      fixedBurnSpike.getBurnTime();
                  if(isNOI) 
                    sc.collectFireSpike(
                        n.getId(), 
                        fixedBurnSpike.getSyn().getAxonNeuronId(),
                        fixedBurnSpike.getBurnTime(), 
                        nMan.getMaxN(), 
                        nMan.getCompressionFactor(),
                        (fixedBurnSpike.getSyn().getAxonNeuronId()<n.getExcitatory()),
                        (fixedBurnSpike.getSyn().getAxonNeuronId()>=n.getN()) );
                }
                continue;
              }
            }
            else if (minFixedBurnTime<Double.MAX_VALUE) {
              FixedBurnSpike fixedBurnSpike = burningQueueSpikes.poll();
              currentTime=minFixedBurnTime;
              burnNeuron(
                  fixedBurnSpike.getSyn(),
                  fixedBurnSpike.getBurnTime(),
                  fixedBurnSpike.getFireTime(),
                  false);
              if(
                  (! lastCollectedBurstFiringNodeId.equals(n.getId()))||
                  (! lastCollectedBurstFiringNeuronId.equals(
                      fixedBurnSpike.getSyn().getAxonNeuronId()))||
                  (! lastCollectedBurstFiringBurnTime.equals(
                      fixedBurnSpike.getBurnTime()))
              ){
                lastCollectedBurstFiringNodeId=n.getId();
                lastCollectedBurstFiringNeuronId=
                    fixedBurnSpike.getSyn().getAxonNeuronId();
                lastCollectedBurstFiringBurnTime=
                    fixedBurnSpike.getBurnTime();
                if(isNOI) 
                  sc.collectFireSpike(
                      n.getId(), 
                      fixedBurnSpike.getSyn().getAxonNeuronId(),
                      fixedBurnSpike.getBurnTime(), 
                      nMan.getMaxN(), 
                      nMan.getCompressionFactor(),
                      (fixedBurnSpike.getSyn().getAxonNeuronId()<n.getExcitatory()),
                      (fixedBurnSpike.getSyn().getAxonNeuronId()>=n.getN()) );
              }
              continue;
            }
            else {
              stopped=true;
              break;
            }
          }
          if ((tmpMinFiringTime==null)||(tmpMinFiringTime>stopTime)){
            stopped=true;
            break;
          }
          //get the next neuron ready to fire in the list of 
          //the active neurons
          //debprintln("\n\ngetting next firing neuron...");
          minFiringNeuron=nnMan.getNextFiringNeuron();
          if (minFiringNeuron==null){
            stopped=true;
            break;
          }
          //debprintln("got next firing neuron.");
          firingNeuronId=minFiringNeuron.fn;
          spikeTime=minFiringNeuron.tf;
          if (spikeTime>stopTime){
            stopped=true;
            break;
          }
        }
        else{
          break;
        }
        //case of first firing of a burst
        //time update
        currentTime = spikeTime;
        //firing spikes detecting and storing
        if (firingNeuronId<n.getN()){
          //State resetting to passive mode
          nnMan.resetState(firingNeuronId);
          nnMan.resetTimeToFire(firingNeuronId);
        }
        // last firing time for neuron
        nnMan.setLastFiringTime(firingNeuronId, currentTime);
        if (firingNeuronId>=n.getN()){
          //ext time-to-fire resetting
          nnMan.resetTimeToFire(firingNeuronId);
          //external routine
          nnMan.extInputReset(firingNeuronId, currentTime);
          if (currentTime>n.getExternalInput().getFireDuration())
            continue;
        }
        if(isNOI) 
          sc.collectFireSpike(
              n.getId(), 
              firingNeuronId, 
              spikeTime, 
              nMan.getMaxN(), 
              nMan.getCompressionFactor(),
              (firingNeuronId<n.getExcitatory()),
              (firingNeuronId>=n.getN()) );
        //search for burning neurons connected to neuron_id
        makeNeuronFire(firingNeuronId, currentTime);
      }
      completed();
      stopped=false;
    }
  }
  
  /**
   *  Adds a new internode fire spike
   *  @param syn      the inter-node synapse object through wich the 
   *                  spike signal is sent
   *  @param fireTime the fire-spike generation time
   */
  private void addInterNodeFire(Synapse syn, Double fireTime){
    Double axonalDelay=synMan.getAxDelay(syn);
    internodeFires.add(new InterNodeSpike(syn, fireTime+axonalDelay,fireTime,axonalDelay));
  }
  
  /**
   *  @return true if this node has still any inter-node spike to be 
   *          processed
   */
  public Boolean hasInterNodeSpikes(){
    if (internodeFires.size()>0)
      return true;
    return false;
  }
  
  
  /**
   * @return the list of internode spikes and clean it
   */
  public ArrayList<InterNodeSpike> pullInternodeFires() {
    ArrayList<InterNodeSpike> retval=internodeFires;
    internodeFires=new ArrayList<InterNodeSpike>();
    return retval;
  }
  
  /*
   * @return the current simulation time (which is the time of the 
   *        current fire event being processed)
   */
  public double getCurrentTime() {
    return currentTime;
  }

  /**
   * Updates the current time
   * @param currentTime the updated current time
   */
  public void setCurrentTime(double currentTime) {
    this.currentTime = currentTime;
  }
  
  /**
   * @return the current split stop time
   */
  public double getStopTime() {
    return stopTime;
  }

  /**
   * set the next split stop time
   * @param stopTime the new stop time defined for the next split
   */
  public void setStopTime(double stopTime) {
    this.stopTime = stopTime;
  }

  /**
   * @return the (unique) node id for the current node
   */
  public Integer getNodeId(){
    return n.getId();
  }
  
  /**
   * create and adds a new inter-node synapse
   * @param firingNodeId     the id of the firing node
   * @param firingNeuronId   the id of the firing neuron (within a node)
   * @param burningNodeId    the id of the burning node
   * @param burningNeuronId  the id of the burning neuron (within a node) 
   * @param presynaptic_w    the presynaptic weight of the synapse
   * @param mu               the postsynaptic weight for the synapse
   * @param lambda           the avg length of the inter-node axon
   */
  public void addInterNodeSynapse(
      Integer firingNodeId, 
      Long firingNeuronId, 
      Integer burningNodeId, 
      Long burningNeuronId,
      Double presynaptic_w,
      Double mu,
      Double lambda) {
    synMan.addInterNodeSynapse(
        firingNodeId, 
        firingNeuronId, 
        burningNodeId, 
        burningNeuronId,
        mu,
        n.getPresynapticForNeuron(firingNeuronId),
        lambda);
  }
  
  public Long getN(){
    return n.getN();
  }
  
  public Long getExcitatory(){
    return n.getExcitatory();
  }
  
  public Long getInhibithory(){
    return n.getInhibithory();
  }
  
  public Double getExcitatoryPresynapticWeight() {
    return n.getExc_ampl();
  }

  public boolean hasExternalInput() {
    return n.hasExternalInput();
  }
  
  public Integer getExternalInputs(){
    return n.getExternalInputs();
  }
  
  /**
   * Plasticity Rule.
   * Multiplicative Learning Rule using STDP (soft-bound) Spike time 
   * depending plasticity
   * 
   *  LTP: Pw = Pwold + (pwmax - Pwold)*Etap*(-delta/taup)
   *  LTD: Pw = Pwold - Pwold*Etam*(delta/taum)
   *  with delta = tpost - tpre
   *
   *  NB: in the case of LTD, tpost represents the burning neuron last burning 
   *  time, whereas tpre is the current "tempo".
   *  This rule is applied for only exc-exc intermolule connections   
   *
   * @param spikingNeuronId
   * @param presentNeuronId
   * @param currentTime
   */
  
  
  /**
   * Plasticity rule for firing events.
   * Update postsynaptic weight, increasing it according to the delta i
   * between the firing time 
   * and the last burning time of the firing neuron.
   * 
   * @param syn
   * @param fireTime
   */
  private void fire_ltp(Synapse syn, Double fireTime){
    if (!plasticity)
      return;
    if (syn.getFiring()==null)
      return;
    Long firingNeuronId = syn.getFiring();
    ArrayList <Synapse> synapses = 
        synMan.getFiringNeuronSynapses(firingNeuronId);
    ArrayList <Synapse> interNodeSynapses = 
        synMan.getFiringNeuronInterNodesSynapses(firingNeuronId);
    for(int i=0; i<synapses.size();++i){
      if (synapses.get(i).getLastBurningTime()==null)
        continue;
      Double delta;
      delta=fireTime-synapses.get(i).getLastBurningTime();
      if (delta < n.getPlasticityTo()){
        Double wp=synapses.get(i).getPostSynapticWeight();
        double wpold=wp;
        wp+=do_fast?
            (n.getPwMax()-wp)*n.getEtap()*fm.fastexp(-delta/n.getTaup()):
              (n.getPwMax()-wp)*n.getEtap()*Math.exp(-delta/n.getTaup());
        synMan.setIntraNodePostSynapticWeight(synapses.get(i),wp);
      }
    }
    for(int i=0; i<interNodeSynapses.size();++i){
      if (interNodeSynapses.get(i).getLastBurningTime()==null)
        continue;
      Double delta;
      delta=fireTime-interNodeSynapses.get(i).getLastBurningTime();
      if (delta < n.getPlasticityTo()){
        Double wp=interNodeSynapses.get(i).getPostSynapticWeight();
        double wpold=wp;
        wp+=do_fast?
            (n.getPwMax()-wp)*n.getEtap()*fm.fastexp(-delta/n.getTaup()):
              (n.getPwMax()-wp)*n.getEtap()*Math.exp(-delta/n.getTaup());
        synMan.setIntraNodePostSynapticWeight(
            interNodeSynapses.get(i),
            wp);
      }
    }
  }

  /**
   * Plasticity rule for burning events.
   * Update postsynaptic weight, decreasing it according to 
   * the delta between the burning time 
   * and the last firing time of the burning neuron.
   * @param syn
   * @param lastBurningTime
   * @param fireTime
   */
  private void burning_ltd(
      Synapse syn, 
      Double burningTime, 
      Double lastFiringTime){
    if (!plasticity)
      return; 
    if (syn.getBurning()==null)
      return;
    syn.setLastBurningTime(burningTime);
    Double delta;
    delta=burningTime-lastFiringTime;
    if (delta < n.getPlasticityTo()){
      Double wp=syn.getPostSynapticWeight();
      double wpold=wp;
      wp -= do_fast?
          wp*n.getEtam()*fm.fastexp(-delta/n.getTaum()):
           wp*n.getEtam()*Math.exp(-delta/n.getTaum());
      if (wp<0)
        wp=0.0;
      synMan.setIntraNodePostSynapticWeight(syn,wp);
    }
  }
  
  
  /**
   * Elicit a spike from the firing neuron to each connected burning.
   * 
   * @param firingNeuronId
   * @param currentTime
   */
  private void makeNeuronFire(Long firingNeuronId, Double currentTime){
    ArrayList <Synapse> synapses = 
        synMan.getFiringNeuronSynapses(firingNeuronId);
    ArrayList <Synapse> interNodeSynapses = 
        synMan.getFiringNeuronInterNodesSynapses(firingNeuronId);
    if (n.isExternalInput(firingNeuronId)){
      int eod=n.getExternalOutDegree();
      int eoj=n.getExternalOutJump();
      if (eod==1){
        burnNeuron(
            null,
            firingNeuronId, 
            n.getId(), 
            firingNeuronId%n.getN(), 
            n.getId(), 
            0.1,
            1.0,
            n.getExternalAmplitude(),
            currentTime, 
            currentTime, 
            true);
      }
      else
        for (int i=0; i<eod; ++i){
          burnNeuron(
              null,
              firingNeuronId, 
              n.getId(), 
              (firingNeuronId+(eoj*i))%n.getN(), 
              n.getId(), 
              0.1,
              1.0,
              n.getExternalAmplitude(),
              currentTime, 
              currentTime, 
              true);
        }
      return;
    }
    for (int i=0; i<synapses.size();++i){
      fire_ltp(
          synapses.get(i), 
          currentTime);
      // this is an inter-node synapse, the burning node 
      // must deal with this spike
      if (!(synapses.get(i).getBurningNodeId().equals(n.getId()))){
        continue;
      }
      burnNeuron(synapses.get(i), currentTime, currentTime, false);
      for (int j=1; j<n.getBn(); ++j)
        burningQueueSpikes.add(
            new FixedBurnSpike(synapses.get(i), 
                (currentTime+n.getIBI()*j), 
                currentTime));
    }
    for (int i=0; i<interNodeSynapses.size();++i){
      for (int j=0; j<n.getBn(); ++j)
        addInterNodeFire(interNodeSynapses.get(i), (currentTime+n.getIBI()*j));
    }
  }
  
  public void burnNeuron(
        Synapse s, 
        Double burnTime, 
        Double fireTime, 
        Boolean fromExternalInput){
    burnNeuron(
        s,
        s.getFiring(),
        s.getFiringNodeId(),
        s.getBurning(),
        s.getBurningNodeId(),
        s.getLength(),
        s.getPostSynapticWeight(),
        s.getPreSynapticWeight(),
        burnTime,
        fireTime,
        fromExternalInput);
  }


  public void burnNeuron(
      Synapse s, 
      long firingNeuronId,
      int firingNodeId,
      long burningNeuronId,
      int burningNodeId,
      double axon_length,
      double postsynapticWeight,
      double presynapticWeight,
      double burnTime, 
      double fireTime, 
      boolean fromExternalInput){
    double tmp, dsxNumerator, dsxDenominator, riseTermXFactor, oldSx;
    int arp;
    //distinguish cases of no initial network activity : already activated
    arp=(nnMan.getLastFiringTime(burningNeuronId).equals(Constants.TIME_TO_FIRE_DEF_VAL))?0:1;
    //absolutely refractory period check
    if (burnTime>=(( 
        nnMan.getLastFiringTime(burningNeuronId)+
        nnMan.getT_arp()+
        ((n.getBn()-1)*n.getIBI()))
        *arp) ){
      long startTime = System.currentTimeMillis();
      if (!fromExternalInput)
        burning_ltd(
            s, 
            burnTime, 
            nnMan.getLastFiringTime(burningNeuronId));
      tmp=nnMan.getState(burningNeuronId);
      //passive state linear decay
      if (tmp<nnMan.getSpikingThr()){
        Double decay;
        //linear decay
        if (!exp_decay){
            decay = (
                nnMan.getLinearDecayD(burningNeuronId)*
                (burnTime-
                        (nnMan.getLastBurningTime(
                                burningNeuronId))));
                nnMan.setState(
                        burningNeuronId, 
                        tmp-decay);
        }
        //exponential decay
        //Sj = Spj + A * W -Tl =  A W + Spj e^(-delta t / D)
        //Tl  =  Spj (1 - e^(-delta t / D))
        //if (exp_decay){
        else{
            decay = do_fast? (
                tmp * (
                    1 - fm.fastexp(
                        - ( burnTime -
                            nnMan.getLastBurningTime(burningNeuronId)
                        )/
                        nnMan.getLinearDecayD(burningNeuronId)))
            ):(
                tmp * (
                    1 - Math.exp(
                        -(burnTime-
                            nnMan.getLastBurningTime(burningNeuronId)
                        )/
                        nnMan.getLinearDecayD(burningNeuronId)))
            );
            nnMan.setState(
                burningNeuronId, 
                tmp-decay);
        }
        if (nnMan.getState(burningNeuronId)<0.0)
          nnMan.setState(burningNeuronId, 0.0);
      }
      times[0]+=System.currentTimeMillis()-startTime;
      startTime = System.currentTimeMillis();
      
      //BURNING NEURON
      double sx = nnMan.getState(burningNeuronId);
      oldSx=sx;
      //step in state
      double sy = postsynapticWeight*presynapticWeight;
      // UPDATING List of Active Neurons
      // case of passive neuron
      if (nnMan.getTimeToFire(burningNeuronId)
          .equals(Constants.TIME_TO_FIRE_DEF_VAL)){
        oldSx=sx;
        sx = ((sx+sy)<0)?0:sx+sy;
        nnMan.setState(burningNeuronId, sx);
        //passive to active
        if (sx>=nnMan.getSpikingThr()){
          //nnMan.setTimeToFire(s.getBurning(), burnTime+ 1.0/(sx-1));
          if (lif)
            nnMan.setTimeToFire(
                burningNeuronId, 
                burnTime+Constants.EPSILON);
          else{
            double activeTransitionDelay=(1.0/(sx-1));
            nnMan.setTimeToFire(
                burningNeuronId, 
                burnTime+ activeTransitionDelay);
          }
          //if(isNOI) 
          //  sc.collectPassive2active();
          nnMan.addActiveNeuron(
              burningNeuronId, 
              nnMan.getTimeToFire(burningNeuronId), 
              currentTime, 
              2);
        }
        //else{
        //  if(isNOI) 
        //    sc.collectPassive();
        //}
        times[1]+=System.currentTimeMillis()-startTime;
      }
      // case of active neuron
      // avoid update on lif
      else if (!lif){
      //else {
        if (nnMan.getTimeToFire(burningNeuronId)==0.0)
          nnMan.setTimeToFire(burningNeuronId, Constants.EPSILON);
        if (sx>=nnMan.getSpikingThr()){
          nnMan.removeActiveNeuron(burningNeuronId);
          if ( (burnTime < nnMan.getTimeToFire(burningNeuronId) )
              && (!nnMan.getLastBurningTime(burningNeuronId)
                  .equals(Constants.BURNING_TIME_DEF_VAL)) ){
            //Rise Term
            riseTermXFactor=
                (burnTime==nnMan.getLastBurningTime(burningNeuronId))?
                    Constants.EPSILON : (
                        burnTime-nnMan.getLastBurningTime(burningNeuronId));
            tmp=(sx-1)*riseTermXFactor;
            dsxNumerator = (sx-1)*tmp;
            dsxDenominator= 1.0-tmp;
            sx+=(dsxNumerator/dsxDenominator);
          }
          oldSx=sx;
          sx += sy;
          nnMan.setState(burningNeuronId,sx);
          //active to passive
          if (sx<nnMan.getSpikingThr()){
            nnMan.removeActiveNeuron(burningNeuronId);
            nnMan.resetTimeToFire(burningNeuronId);
            //if (isNOI)
            //  sc.collectActive2passive();
          }
          else{
            //updating firing delay
            nnMan.setTimeToFire(burningNeuronId, burnTime + 1.0/(sx-1));
            nnMan.setState(burningNeuronId, sx);
            nnMan.addActiveNeuron(
                burningNeuronId, 
                nnMan.getTimeToFire(burningNeuronId), 
                currentTime, 
                3);
             //if(isNOI) 
             // sc.collectActive();
          }
          //active to passive
          if (sx<0){
            sx=0.0;
            oldSx=sx;
            nnMan.setState(burningNeuronId,sx);
            nnMan.removeActiveNeuron(burningNeuronId);
            nnMan.resetTimeToFire(burningNeuronId);
            //if(isNOI) 
            //  sc.collectActive2passive();
          }
        }
        else{
          oldSx=sx;
          nnMan.removeActiveNeuron(burningNeuronId);
          nnMan.resetTimeToFire(burningNeuronId);
          //if(isNOI) 
          //  sc.collectActive2passive();
        }
        times[2]+=System.currentTimeMillis()-startTime;
      }
      //end of case of active neuron
      startTime = System.currentTimeMillis();
      nnMan.setLastBurningTime(burningNeuronId, burnTime);
      times[4]+=System.currentTimeMillis()-startTime;
      // collecting the spike
      if(isNOI) 
        sc.collectBurnSpike(
            firingNeuronId,
            firingNodeId,
            burningNeuronId,
            burningNodeId,
            burnTime,
            fromExternalInput, 
            oldSx, 
            sy, 
            postsynapticWeight,
            presynapticWeight,
            nnMan.getTimeToFire(burningNeuronId),
            fireTime);
      times[3]+=System.currentTimeMillis()-startTime;
    }
    else{
      // collecting the spike
      if(isNOI) 
        sc.collectBurnSpike(
            firingNeuronId,
            firingNodeId,
            burningNeuronId,
            burningNodeId,
            burnTime,
            fromExternalInput, 
            null, 
            null, 
            postsynapticWeight,
            presynapticWeight,
            nnMan.getTimeToFire(burningNeuronId),
            fireTime);
    }
  }
  
  
  public void burnInterNodeSpike(InterNodeSpike irs){
    // the comparison is done against the stoptime, since when this method is called 
    // it still holds the value for the last split run (not the current one) and
    // current time may hold a very old value.
    if (irs.getBurnTime()>=stopTime)
      interNodeBurningSpikes.add(new InterNodeBurningSpike(irs, irs.getBurnTime()));
    else{
      sc.collectMissedFire(irs.getAxonalDelay());
      println("missed fire at time:"+
            irs.getBurnTime()+
            ", axonal del:"+
            irs.getAxonalDelay()+
            ", fire time:"+
            irs.getFireTime()+
            ", current time:"+
            currentTime+
            ", syn:"+
            irs.getSyn());
    }
  }
  
  private void initExtInput(){
    println("initializing external input...");
    for (int j=0; j<n.getExternalInputs();++j){
      nnMan.extInputReset(n.getN()+j,0.0);
    }
    println("external input initialization done.");
  }
  
  
  //=======================================  thread functions  =======================================
  
  public void runNewSplit(double newStopTime){
    /* 
     * current time is updated with the last value for stoptime, ccause otherwise it can hold
     * value belonging to a past split and then could generate inconsistency against
     * spikes coming frmo external nodes
     */
    startTime=stopTime;
    if (currentTime<stopTime)
      currentTime=stopTime;
    stopTime = newStopTime;
    lock.lock();
    newTimeSplitCond.signal();
    lock.unlock();
  }

  private void completed(){
    double oldStopTime=stopTime;
    print_times();
    if (nMan.splitComplete(getNodeId())){
      lock.lock();
      try {
        newTimeSplitCond.await();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      lock.unlock();
    }
    else{
      while (keepRunning && (stopTime<=oldStopTime) ){
        try {
          Thread.sleep(100);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }
  }
  
  public void kill(){
    keepRunning=false;
    lock.lock();
    newTimeSplitCond.signal();
    lock.unlock();
  }
  
  
  
  
  //=======================================  printing function =======================================
  
  
  public void printQueues(){
    Iterator<Synapse> it = queuesMap.keySet().iterator();
    println("Printing queues:");
    while (it.hasNext()){
      Synapse s = it.next();
      System.out.println(s + ": "); 
      ((NiceQueue)queuesMap.get(s)).printQueue();
          
    }
    System.out.println();
  }
  
  private void println(String s){
    if (verbose){
      if (nMan != null)
        System.out.println(TAG+getNodeId()+"/"+(nMan.getNodeThreadsNum()-1)+"] "+s);
      else 
        System.out.println(TAG+getNodeId()+"/-] "+s);
    }
  }
  
  private void debprintln(String s){
    if (verbose&&debug)
      System.out.println(TAG+getNodeId()+"/"+(nMan.getNodeThreadsNum()-1)+" [debug] ] "+s);
  }
  
  private void leveldebprintln(String s, int level){
    if (this.debug_level>=level)
     debprintln(s);
  }
  
  private void debActiveprintln(String s){
    if (verbose&&debug&&activePassiveDebug)
      System.out.println(TAG+getNodeId()+"/"+(nMan.getNodeThreadsNum()-1)+"[debug] ] "+s);
  }

  public void printDebug(){
    println("max pw approximation:"+debugMaxWPDiff);
    System.out.println();
  }
  
  public void print_times() {
    return;
//    for (int i=0; i<5;++i)
//      println("times["+i+"]: "+times[i]);
  }

  
  
  
  
  


}
