/**
* "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.node.neuron;

import java.util.HashMap;
import spiking.node.Node;
import spiking.node.external_inputs.ExternalInput;
import utils.constants.Constants;
import utils.experiment.Experiment;
import utils.tools.NiceNode;
import utils.tools.NiceQueue;
import org.apache.commons.math3.distribution.ExponentialDistribution;


public class NodeNeuronsManager {

  private final static String TAG = "[Neurons Manager for Node: ";
  private final static Boolean verbose = true;

  private Node n;
  private Boolean debug = false;
  //linear decay constant
  private Double D_exc=0.001;
  private Double D_inh=0.001;
  //threshold
  private Double c=0.04;
  //refractory time constant
  private Double t_arp=0.3;
  //spiking threshold
  private Double sth=1+c;
  //the list of active neurons - a map ordered on time 
  //values (as key) mapping neuronsId
  private NiceQueue activeNeurons;
  private Double excitatoryPresynapticDefVal=2.0;
  private Double inhibithoryPresynapticDefVal=-1.0;
  private Double externalSourcesPresynapticDefVal=1.0;
  //the neuron states for the node
  private HashMap<Long, Double> neuronStates;
  private HashMap<Long, Double> timesToFire;
  private HashMap<Long, Double> lastFiringTimes;
  private HashMap<Long, Double> lastBurningTimes;
  private HashMap<Long, Double> presynapticWeights;
  private ExponentialDistribution exponentialD;
  
  public NodeNeuronsManager(
      Node r, 
      Double c, 
      Double D_exc, 
      Double D_inh, 
      Double t_arp, 
      Double excitatoryPresynapticDefVal, 
      Double inhibithoryPresynapticDefVal,
      Double externalPresynapticDefVal){
    this.n=r;
    this.c=c;
    this.sth=1+c;
    this.D_exc=D_exc;
    this.D_inh=D_inh;
    this.t_arp=t_arp;
    this.excitatoryPresynapticDefVal=excitatoryPresynapticDefVal;
    this.inhibithoryPresynapticDefVal=inhibithoryPresynapticDefVal;
    this.externalSourcesPresynapticDefVal = externalPresynapticDefVal;
    activeNeurons=new NiceQueue("activeNeurons-"+r.getId());
    init();
  }
  
  private void init(){
    neuronStates = new HashMap<Long, Double>();
    timesToFire = new HashMap<Long, Double>();
    lastFiringTimes = new HashMap<Long, Double>();
    lastBurningTimes = new HashMap<Long, Double>();
    presynapticWeights = new HashMap<Long, Double>();
    exponentialD = new ExponentialDistribution(
        n.getExternalInput().getTimeStep()); 
  }
  
  public Double getT_arp(){
    return t_arp;
  }

  public Double getSpikingThr() {
    return sth;
  }
  
  public Double getLinearDecayD(Long neuronId){
    return (n.isExcitatory(neuronId))? D_exc: D_inh;
  }
  
  public Double getLinearDecayD_exc(){
    return D_exc;
  }
  
  public Double getLinearDecayD_inh(){
    return D_inh;
  }

  public Double getExcitatoryPresynapticDefVal() {
    return excitatoryPresynapticDefVal;
  }

  public Double getInhibithorySynapticDefVal() {
    return inhibithoryPresynapticDefVal;
  }

  public Double getExternalSourcesPresynapticDefVal() {
    return externalSourcesPresynapticDefVal;
  }
  
  public void setTimeToFire(Long neuronId, Double val){
    timesToFire.put(neuronId, val);
  }
  
  public Double getTimeToFire(Long neuronId){
    Double retval = timesToFire.get(neuronId);
    if (retval==null){
      if (neuronId<n.getN())
        return Constants.TIME_TO_FIRE_DEF_VAL;
      return Constants.EXTERNAL_TIME_TO_FIRE_DEF_VAL;
    }
    return retval;
  }
  
  public void setPreSynapticWeight(Long neuronId, Double val){
    presynapticWeights.put(neuronId, val);
  }
  
  public Double getPreSynapticWeight(Long neuronId){
    Double retval = presynapticWeights.get(neuronId);
    if (retval==null)   
      retval = (isExcitatory(neuronId))? 
          n.getExc_ampl(): 
          n.getInh_ampl();
    return retval;
  }
  
  public Double getState(Long neuronId){
    Double retval = neuronStates.get(neuronId);
    if (retval==null){
      Double lastBurningTime=lastBurningTimes.get(neuronId);
      if (lastBurningTime==null){
        neuronStates.put(neuronId, Math.random());
        retval=neuronStates.get(neuronId);
      }
      else
        retval=0.0;
    }
    return retval;
  }
  
  public void setState(Long neuronId, Double val) {
    neuronStates.put(neuronId, val);
  }
  
  
  public void resetState(Long neuronId) {
    neuronStates.put(neuronId, 0.0);    
  }
  
  public void resetTimeToFire(Long neuronId){
    timesToFire.remove(neuronId);
  }
  
  
  public void setLastFiringTime(Long neuronId, Double val){
      lastFiringTimes.put(neuronId, val);
  }
  
  public Double getLastFiringTime(Long neuronId){
    Double retval = lastFiringTimes.get(neuronId);
    if (retval==null)
      retval=Constants.TIME_TO_FIRE_DEF_VAL;
    return retval;
  }
  
  public NiceNode getNextFiringNeuron() {
    return activeNeurons.extractMin();
  }
  
  
  /** 
   * @return the minimum time to fire for active neurons, 
   * without polling the value from the queue 
   * If the queue is empty, null is returned                                                                           
   */
  public Double getMinFiringTime(){
    return activeNeurons.getMinTime();
  }
  
  public boolean isExcitatory(Long neuronId){
    return n.isExcitatory(neuronId);
  }
  
  public Double getLastBurningTime(Long neuronId) {
    Double retval = lastBurningTimes.get(neuronId);
    if (retval==null)
      retval=0.0;
    return retval;
  }
  
  public void setLastBurningTime(Long neuronId, Double lastBurningTime){
    this.lastBurningTimes.put(neuronId,lastBurningTime); 
  }
  
  /**
   * @param extNeuronId   the id of the external input as 
   *                      referred into the node reg
   * @param currentTime   the current simulation time
   */
  public void extInputReset(Long extNeuronId, double currentTime){
    if (currentTime>n.getExternalInput().getFireDuration()){
      removeActiveNeuron(extNeuronId);
      return;
    }
    double fireTime;
    // case of constant external inputs
    if (n.getExternalInputsType()==ExternalInput.CONSTANT){
      if (currentTime==0.0)
        fireTime=n.getExternalInputsTimeOffset(extNeuronId);
      else
        fireTime=currentTime+n.getExternalInput().getTimeStep();
      setPreSynapticWeight(
          extNeuronId, 
          n.getAmplitudeValue(extNeuronId));
      setTimeToFire(extNeuronId, fireTime);
      addActiveNeuron(extNeuronId, fireTime, currentTime, 0);
      return;
    }
    // case of noise external inputs
    else if (n.getExternalInputsType()==ExternalInput.NOISE){
      if (currentTime==0.0)
        fireTime=
            n.getExternalInputsTimeOffset(extNeuronId)+
            Math.random()*
            2*
            n.getExternalInput().getTimeStep();
      else
        fireTime=
            currentTime+
            Math.random()*
            2*
            n.getExternalInput().getTimeStep();
      setPreSynapticWeight(
          extNeuronId, 
          n.getAmplitudeValue(extNeuronId));
      setTimeToFire(extNeuronId, fireTime);
      addActiveNeuron(extNeuronId, fireTime, currentTime, 4);
      return;
    }
    /* Case of poissonian external inputs */
    fireTime = currentTime+((double)(exponentialD.sample()));
    setPreSynapticWeight(extNeuronId, n.getAmplitudeValue(extNeuronId ));
    setTimeToFire(extNeuronId, fireTime);
    addActiveNeuron(extNeuronId, fireTime, currentTime, 1);
  }
  
  
  public void addActiveNeuron(Long neuronId, Double fireTime, Double currentTime, Integer debug){
    if (fireTime<currentTime) {
      println("\n....................\ndebug"+debug+"\n....................");
      println("fire time:"+fireTime+" current time:"+currentTime);
      System.exit(1);
    }
    activeNeurons.insert(fireTime, neuronId);
  }
  
  public void removeActiveNeuron(Long neuronId){
    activeNeurons.delete(neuronId);
  }
  
  public int getActiveNeuronsNum(){
    return activeNeurons.size();
  }
  
  
  
  //=======================================   printing functions =======================================
  
  
  private void println(String s){
    if (verbose)
      System.out.println(TAG+n.getId()+"] "+s);
  }
  
  private void debprintln(String s){
    if (verbose&&debug)
      System.out.println(TAG+n.getId()+"][debug] "+s);
  }
  
  public void printAn(){
    println("printing active neurons:");
    NiceNode tmp [] = activeNeurons.toArray();
    for (int i=0; i<tmp.length; ++i)
        System.out.println("active neuron "+i+":\t"+tmp[i].toString()+", state:"+getState(tmp[i].fn));  
  }

  
  

}