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

import java.io.File;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Random;
import utils.experiment.Experiment;
import utils.tools.LongCouple;
import utils.tools.NiceNode;
import utils.tools.Shuffler;
import spiking.node.external_inputs.ExternalInput;
import org.mapdb.*;


public class Node {
  
  private final static String TAG = "[Node ";
  private final static Boolean verbose = true;
  private Integer id;
  private ExternalInput ext;
  //number and kind of neurons 
  private Long n=100l;
  private Long excitatory;
  private Long inhibithory;
  //Bursting Node Cardinality
  private Integer Bn;
  // InterBurst Interval
  private Double IBI;
  //the small world connection matrix
  private HashMap<LongCouple, Double> connectionMatrix = new HashMap<>();
  //proportion between excitatory and inhibithory
  private Double R=0.8;
  // postsynaptic weight
  private Double mu_w_exc;
  private Double mu_w_inh;
  // postsynaptic std dev
  private Double sigma_w_exc;
  private Double sigma_w_inh;
  //excitatory amplitude
  private Double w_pre_exc;
  //inhibitory amplitude
  private Double w_pre_inh;
  //number of external inputs
  private Integer externalInputs=0;  
  //edges per vertex mean degree
  private Integer k=20;  
  //rewiring probability
  private Double prew=0.5;
  private Boolean hasExternalInputs = false;
  private int externalInputType=-1;
  private Double externalInputsTimeOffset;
  private double timeStep = -1;
  private int fireDuration = 1;
  private Double externalAmplitude=
      ExternalInput.EXTERNAL_AMPLITUDE_DEF_VALUE;
  private int externalOutdegree = 0;
  private int externalOutJump = 1;
  private Boolean plasticity;
  private Double etap;
  private Double etam;
  private Double taup;
  private Double taum;
  private Double pwMax;
  private Double to;
  private HashMap <Long,Boolean> external_init= 
      new HashMap <Long, Boolean>();
  private Random random = new Random();

  /**
  *   The Node object
  *
  *   @param id           the node id
  *   @param n            the number of neurons of the node
  *   @param R            the ratio of excitatory neurons over the total
  *                       number of neurons 'n'
  *   @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 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 plasticity   simulate neuron plasticity
  *   @param etap         the Eta plus learning constant for plasticity
  *   @param etam         the Eta minus learning constant for plasticity
  *   @param taup         the Tau plus positive time constants for long term 
  *                       potentiation for plasticity
  *   @param taum         the Tau minus positive time constants for long term 
  *                       depression for plasticity
  *   @param pwMax        the max post-ynaptic weight (used for plasticity rules)
  *   @param to           the timeout for plasticity rules
  *
  */
  public Node(
      Integer id, 
      Long n, 
      Double R,
      Double mu_w_exc,
      Double mu_w_inh,
      Double sigma_w_exc,
      Double sigma_w_inh,
      Double w_pre_exc,
      Double w_pre_inh,
      Integer k, 
      Double prew, 
      Integer Bn, 
      Double IBI,
      Boolean plasticity,
      Double etap,
      Double etam,
      Double taup,
      Double taum,
      Double pwMax,
      Double to){
    this.id=id;
    this.n=n;
    this.R=R;
    this.mu_w_exc=mu_w_exc;
    this.mu_w_inh=mu_w_inh;
    this.sigma_w_exc=sigma_w_exc;
    this.sigma_w_inh=sigma_w_inh;
    this.w_pre_exc=w_pre_exc;
    this.w_pre_inh=w_pre_inh<0?w_pre_inh:(-w_pre_inh);
    this.k=k;
    this.prew=prew;
    this.Bn=Bn;
    this.IBI=IBI;
    this.plasticity=plasticity;
    this.etap=etap;
    this.etam=etam;
    this.taup=taup;
    this.taum=taum;
    this.pwMax=pwMax;
    this.to=to;
    initExcitInhib();
    nodeInit();
  }
  
  public Node(
      Integer id, 
      Long n, 
      Integer externalInputs, 
      int externalInputType,
      Double externalInputsTimeOffset,
      double timeStep, 
      int fireDuration, 
      Double externalAmplitude,
      Integer externalOutdegree,
      Double R, 
      Double mu_w_exc,
      Double mu_w_inh,
      Double sigma_w_exc,
      Double sigma_w_inh,
      Double w_pre_exc,
      Double w_pre_inh,
      Integer k, 
      Double prew, 
      Integer Bn, 
      Double IBI,
      Boolean plasticity,
      Double etap,
      Double etam,
      Double taup,
      Double taum,
      Double pwMax,
      Double to){
    this.id=id;
    this.n=n;
    this.externalInputType=externalInputType;
    this.externalInputsTimeOffset=externalInputsTimeOffset;
    this.R=R;
    this.mu_w_exc = mu_w_exc;
    this.mu_w_inh = mu_w_inh;
    this.sigma_w_exc=sigma_w_exc;
    this.sigma_w_inh=sigma_w_inh;
    this.w_pre_exc=w_pre_exc;
    this.w_pre_inh=w_pre_inh<0?w_pre_inh:(-w_pre_inh);
    if (externalInputs>0){
      hasExternalInputs=true;
      this.externalInputs=externalInputs;
      this.timeStep=timeStep;
      this.fireDuration=fireDuration;
      this.externalAmplitude=externalAmplitude;
      this.externalOutdegree=externalOutdegree;
      do { this.externalOutJump=random.nextInt(1987);}
        while (this.externalOutJump==0);
    }
    this.k=k;
    this.prew=prew;
    this.Bn=Bn;
    this.IBI=IBI;
    this.plasticity=plasticity;
    this.etap=etap;
    this.etam=etam;
    this.taup=taup;
    this.taum=taum;
    this.pwMax=pwMax;
    this.to=to;
    initExcitInhib();
    nodeInit();
    
  }
  
  public void nodeInit(){
    println("init...");
    // ring closure
    wireInit(); 
    externalInputInit();
    println("init done.");
  }
  
  public void initExcitInhib(){
    excitatory= (long) Math.floor(n*R);
    inhibithory=n-excitatory;
  }
  
  private void putConnection (
      Long firingNeuronId, 
      Long burningNeuronId, 
      Double presynaptic_weight){
    connectionMatrix.put(
        new LongCouple(firingNeuronId, burningNeuronId), 
        presynaptic_weight);
  }
  
  public Double getConnectionPresynapticWeight(
      Long firingNeuronId, 
      Long burningNeuronId){
    return (
        connectionMatrix.get(
            new LongCouple(firingNeuronId, burningNeuronId))!=null)?
        connectionMatrix.get(
            new LongCouple(firingNeuronId, burningNeuronId)):0;
  }
  
  public Iterator<Entry<LongCouple, Double>> getIterator(){
    return connectionMatrix.entrySet().iterator();
  }
  
  public Iterator<LongCouple> getKeyConnectionIterator(){
    return connectionMatrix.keySet().iterator();
  }
  
  public Integer getId(){
    return id;
  }
  
  private void wireInit(){
    if (n<=1)
      return;
    println("ring wiring...");
    //randomize adjacency
    DB tmpDb1 = DBMaker.memoryDirectDB().make();
    DB tmpDb2 = DBMaker.memoryDirectDB().make();
    HTreeMap<Long, Long> shuffled = tmpDb1.hashMap(
        "shuffle", 
        Serializer.LONG,Serializer.LONG).create();
    HTreeMap<Long, Long> shuffled_rand = tmpDb2.hashMap(
        "shuffle", 
        Serializer.LONG,Serializer.LONG).create();
    Shuffler.shuffleArray(shuffled,n);
    Shuffler.shuffleArray(shuffled_rand,n);
    int k2=k/2;
    Double tmpAmpl;
    long l=0;
    for (long i=0; i<n;++i){
      Long tmpSrc=shuffled.get(i);
      if (isExcitatory(shuffled.get(i)))
        tmpAmpl=w_pre_exc;
      else
        tmpAmpl=w_pre_inh;
      for (long j=1; j<=k2;++j){
        //rewiring condition
        if (Math.random()<prew){
          Long tmp;
          for (;
              ((tmp = shuffled_rand.get(l) )
                  .equals(tmpSrc)) || 
                  (tmp.equals(shuffled.get((i+j)%n)))||
                  (connectionMatrix.get(
                      new LongCouple(
                          tmpSrc,
                          tmp))!=null);
                l=(l+1)%n){}
          putConnection(shuffled.get(i), tmp, tmpAmpl);
        }
        else
          putConnection(
              shuffled.get(i), 
              shuffled.get((i+j)%n), 
              tmpAmpl);
        if (Math.random()<prew){
          Long tmp;
          for (;
              ((tmp = shuffled_rand.get(l) )
                  .equals(tmpSrc)) || 
                  (tmp.equals(shuffled.get((n+i-j)%n)))||
                  (connectionMatrix.get(
                      new LongCouple(
                          tmpSrc,
                          tmp))!=null);
                l=(l+1)%n){}
          //while ( 
          //    ((tmp = (long) Math.round(Math.random()*(n-1)))
          //    .equals(shuffled.get(i))) || 
          //    (tmp.equals(shuffled.get((n+i-j)%n)))){}
          putConnection(shuffled.get(i), tmp, tmpAmpl);
        }
        else
          putConnection(
              shuffled.get(i), 
              shuffled.get((n+i-j)%n), 
              tmpAmpl);
      }
    }
    shuffled.close();
    println("wiring done.");
  }
  
  private void externalInputInit(){
    if (externalInputs<=0){
      println("d with no external input.");
      return;
    }
    println("creating external input...");
    ext= new ExternalInput(
        this, 
        externalInputType,
        externalInputsTimeOffset,
        fireDuration,
        externalAmplitude,
        externalOutdegree,
        timeStep);
    println(
        "external input created, external spikes in queue:"
        +ext.getExternalSpikesInQueue());
  }

  public Long getN() {
    return n;
  }

  public Long getExcitatory() {
    return excitatory;
  }

  public Long getInhibithory() {
    return inhibithory;
  }

  public Double getExcitProportion() {
    return R;
  }

  public Double getMu_w_exc() {
    return mu_w_exc;
  }

  public Double getMu_w_inh() {
    return mu_w_inh;
  }

  public Double getMu_w_agnostic(Long neuronId) {
    return (isExcitatory(neuronId)?mu_w_exc:mu_w_inh);
  }

  public Double getSigma_w_exc(){
    return sigma_w_exc;
  }

  public Double getSigma_w_inh(){
    return sigma_w_inh;
  }

  public Double getSigma_w_agnostic(Long neuronId) {
    Double retval=isExcitatory(neuronId)?sigma_w_exc:sigma_w_inh;
    return (retval==null)?1:retval;
  }

  public void setMu_w_exc(Double mu_w_exc) {
    this.mu_w_exc = mu_w_exc;
  }

  public void setMu_w_inh(Double mu_w_inh) {
    this.mu_w_inh = mu_w_inh;
  }

  public Double getExc_ampl() {
    return w_pre_exc;
  }

  public Double getInh_ampl() {
    return w_pre_inh;
  }
  
  public Double getPresynapticForNeuron(Long neuronId) {
    return (isExcitatory(neuronId)?w_pre_exc:w_pre_inh);
  }

  public Integer getExternalInputs() {
    return externalInputs;
  }

  public Integer getK() {
    return k;
  }

  public Double getPrew() {
    return prew;
  }
  
  public int getExternalInputsType(){
    return externalInputType; 
  }
  
  public void setStandardExternalInput(){
    ext= new ExternalInput(
        this, 
        externalInputType,
        externalInputsTimeOffset, 
        fireDuration,
        externalAmplitude,
        externalOutdegree,
        timeStep);
  }
  
  public int getExternalOutDegree(){
    return externalOutdegree;
  }

  public int getExternalOutJump(){
    return externalOutJump;
  }

  public Boolean hasExternalInput(){
    return hasExternalInputs;
  }

  public ExternalInput getExternalInput() {
    return ext;
  }
  
  public Double getAmplitudeValue(Long extNeuronGlobalId){
    if ((extNeuronGlobalId-n)>Integer.MAX_VALUE)
      throw new IndexOutOfBoundsException(
          "[NODE ERROR] The external input id is too big");
    if (hasExternalInputs)
      return ext.getAmplitudeValue((int)(long)(extNeuronGlobalId-n));
    return null;
  }

  public Boolean isExternalInput(Long neuronId){
    return neuronId>=n;
  }
  
  public Integer getBn() {
    return (Bn!=null)? Bn : 1 ;
  }

  public Double getIBI() {
    return IBI;
  }
  
  public Boolean getPlasticity(){
    return plasticity;
  }

  public Double getEtap() {
    return etap;
  }

  public Double getEtam() {
    return etam;
  }

  public Double getTaup() {
    return taup;
  }

  public Double getTaum() {
    return taum;
  }

  public Double getPwMax() {
    return pwMax;
  }

  public Double getPlasticityTo() {
    return to;
  }
  
  public Double getExternalAmplitude() {
    return externalAmplitude;
  }
  
  public Double getExternalInputsTimeOffset(Long extNeuronId) {
    Boolean ext_init=external_init.get(extNeuronId);
    double retval=externalInputsTimeOffset;
    if (ext_init==null)
      external_init.put(extNeuronId, true);
    else
      externalInputsTimeOffset=timeStep;
    return retval;
  }
  
  public boolean isExcitatory(Long neuronId){
    if (neuronId<excitatory)
      return true;
    return false;
  }

  public void printNodesConnections(){
    println("printing connections:");
    for (long i=0; i<n; ++i){
      System.out.print(i+".\t");
      for (long j=0; j<n; ++j)
        System.out.print(getConnectionPresynapticWeight(i, j)+", ");
      System.out.println();
    }
  }

  private void println(String s){
    if (verbose)
      System.out.println(TAG+id+"] "+s);
  }
  
}