/*******************************************************************************
    OpenAirInterface
    Copyright(c) 1999 - 2014 Eurecom

    OpenAirInterface is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.


    OpenAirInterface 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 OpenAirInterface.The full GNU General Public License is
    included in this distribution in the file called "COPYING". If not,
    see <http://www.gnu.org/licenses/>.

  Contact Information
  OpenAirInterface Admin: openair_admin@eurecom.fr
  OpenAirInterface Tech : openair_tech@eurecom.fr
  OpenAirInterface Dev  : openair4g-devel@eurecom.fr

  Address      : Eurecom, Campus SophiaTech, 450 Route des Chappes, CS 50193 - 06904 Biot Sophia Antipolis cedex, FRANCE

*******************************************************************************/


/*! \file otg_models.c
* \brief function containing the OTG TX traffic generation functions
* \author M. Laner and navid nikaein
* \date 2013
* \version 0.1
* \company Eurecom
* \email: navid.nikaein@eurecom.fr
* \note
* \warning
*/

#include "UTIL/MATH/oml.h"
#include "otg_models.h"
#include "UTIL/LOG/log.h"

double backgroundRateRnd(void);


/*all the TARMA stuff:
  this is based on the modelling framework provided in
  "M. Laner, P. Svoboda and M. Rupp, Modeling Randomness in Network Traffic, SIGMETRICS/Performance'12, London, UK, 2012".

*/

/* this function must be called for each random process  and each time it shall produce a new random sample,
 which is the return value
 input:   - inputSamples: an array of length TARMA_NUM_PROCESSES with iid gaussian samples
        which must be the same for xcorrelated processes
      - proc: pointer to tarmaProcess_t
 output:  - the next value in the random process
*/
double tarmaCalculateSample( double inputSamples[], tarmaProcess_t *proc)
{
  /* struct with all definitions of the random process and its history */
  int cnt;
  double X, Y, Ypower, Z;

  /* calculate the current sample X' by weighting the input samples */
  X=0;

  for( cnt=0; cnt<TARMA_NUM_PROCESSES ; cnt++) {
    X += proc->inputWeight[cnt]*inputSamples[cnt];
  }

  /* update the MA history: shift everything back for one lag */
  for( cnt=TARMA_NUM_MA_MAX-1; cnt>0; cnt--) {
    proc->maHist[cnt] = proc->maHist[cnt-1];
  }

  proc->maHist[0]=X;  /* update the MA history with the current filter input value */

  /* update the AR history: shift everything back for one lag */
  for( cnt=TARMA_NUM_AR_MAX-1; cnt>0; cnt--) {
    proc->arHist[cnt] = proc->arHist[cnt-1];
  }

  proc->arHist[0]=0;  /* caution: arHist[0] must not be used for the filter processing! */

  /* filter processing */
  Y=0;

  for( cnt=0; cnt<TARMA_NUM_MA_MAX; cnt++) { /* moving average part, start at lag 0 = current X */
    Y += proc->maWeight[cnt] * proc->maHist[cnt];
  }

  for( cnt=1; cnt<TARMA_NUM_AR_MAX; cnt++) { /* auto regressive part, start at lag 1 = former Y */
    Y += -proc->arWeight[cnt] * proc->arHist[cnt];
  }

  proc->arHist[0]=Y;  /* update the AR history with the current filter output value */

  /* polynomial transformation */
  Z=0;
  Ypower=1;

  for(cnt=0; cnt<TARMA_NUM_POLY_MAX; cnt++) {
    Z += proc->polyWeight[cnt]*Ypower;
    Ypower *= Y;
  }

  LOG_D(OTG,"TARMA_DEBUG: tarmaCalculateSamples called: Z=%f\n",Z);
  //tarmaPrintProc(proc);
  return Z;
}

/*
 this function must be called each time before size and itd are generated for the respective packet.
  it updates the input samples stored in the structure stream,
  ps: calling it more often than for each packet is also fine
 input:   - the stream to be updated
 output:
 */
void tarmaUpdateInputSample (tarmaStream_t *stream)
{
  int cnt;
  LOG_T(OTG,"TARMA_DEBUG: tarmaUpdateInputSample(%p)\n", stream);

  if(stream) {
    for(cnt=0; cnt<TARMA_NUM_PROCESSES; cnt++) {
      stream->tarma_input_samples[cnt]=gaussian_dist(10000,1)-10000;
      LOG_D(OTG,"TARMA_DEBUG:   %f\n",stream->tarma_input_samples[cnt]);
    }
  }
}

/*
 this function initializes the given stream, it allocates the respective memory if necessary (stream=0)
  and sets all the respective values to zero.
 input:   - steam to be initialized (or zero if memory shall be allocated)
 output:  - the same stream
 */
tarmaStream_t *tarmaInitStream(tarmaStream_t *stream)
{
  if(stream==0) {
    stream=(tarmaStream_t*) malloc(sizeof(tarmaStream_t));
  }

  tarmaProcess_t *proc;
  int cntvar, cntp, cntma, cntar, cntpy;

  for(cntvar=0; cntvar<2; cntvar++) {
    if(cntvar==0) {
      proc=&(stream->tarma_idt);
    } else {
      proc=&(stream->tarma_size);
    }

    for(cntp=0; cntp<TARMA_NUM_PROCESSES; cntp++) {
      proc->inputWeight[cntp]=0;
    }

    for(cntma=0; cntma<TARMA_NUM_MA_MAX; cntma++) {
      proc->maWeight[cntma]=0;
      proc->maHist[cntma]=0;
    }

    for(cntar=0; cntar<TARMA_NUM_AR_MAX; cntar++) {
      proc->arWeight[cntar]=0;
      proc->arHist[cntar]=0;
    }

    for(cntpy=0; cntpy<TARMA_NUM_POLY_MAX; cntpy++) {
      proc->polyWeight[cntpy]=0;
    }
  }

  LOG_D(OTG,"TARMA_DEBUG: tarmaInitStream(%p) called\n", stream);
  return stream;
}

/*
 this function initializes the input stream according to the openarena traffic modelling
 input:   - stream to be initialized
 output:
 */
void tarmaSetupOpenarenaDownlink(tarmaStream_t *stream)
{
  stream->tarma_size.inputWeight[0]=1;
  stream->tarma_size.maWeight[0]=0.6;
  stream->tarma_size.maWeight[1]=-1.04;
  stream->tarma_size.maWeight[2]=0.44;
  stream->tarma_size.arWeight[0]=1;
  stream->tarma_size.arWeight[1]=-1.971;
  stream->tarma_size.arWeight[2]=0.971;
  stream->tarma_size.polyWeight[0]=157;
  stream->tarma_size.polyWeight[1]=67.2;
  stream->tarma_size.polyWeight[2]=-11.2;
  stream->tarma_size.polyWeight[3]=-1.9;
  stream->tarma_size.polyWeight[4]=18.6;
  stream->tarma_size.polyWeight[5]=1.8;
  //tarmaPrintStreamInit(stream);
}

/*
 this function prints the actual history of a random process
 input:   - random process
 output:
 */
void tarmaPrintProc(tarmaProcess_t *proc)
{
  char prefix[]="OTG TARMA DEBUG: ";
  int cntma, cntar;

  printf("%s tarmaPrintProc(%p) called\n", prefix, proc);
  printf("%s     ma history:\n",prefix);

  for(cntma=0; cntma<TARMA_NUM_MA_MAX; cntma++) {
    printf("%s       ma[%d]: %f\n",prefix,cntma,proc->maHist[cntma]);
  }

  printf("%s     ar history:\n",prefix);

  for(cntar=0; cntar<TARMA_NUM_AR_MAX; cntar++) {
    printf("%s       ar[%d]: %f\n",prefix,cntar,proc->arHist[cntar]);
  }
}

/*
 this function prints the fixed (weigths) parameters of the input stream
 input:   - stream to be printed
 output:
 */
void tarmaPrintStreamInit(tarmaStream_t *stream)
{
  char prefix[]="OTG TARMA DEBUG: ";
  tarmaProcess_t *proc;
  int cntvar, cntp, cntma, cntar, cntpy;

  printf("%s tarmaPrintStreamInit(%p) called\n", prefix, stream);

  for(cntvar=0; cntvar<2; cntvar++) {
    if(cntvar==0) {
      proc=&(stream->tarma_idt);
      printf("%s     variable: idt\n",prefix);
    } else {
      proc=&(stream->tarma_size);
      printf("%s     variable: size\n",prefix);
    }

    printf("%s       input processes\n",prefix);

    for(cntp=0; cntp<TARMA_NUM_PROCESSES; cntp++) {
      printf("%s         w_pr[%d]: %f\n",prefix,cntp,proc->inputWeight[cntp]);
    }

    printf("%s       moving average weights\n",prefix);

    for(cntma=0; cntma<TARMA_NUM_MA_MAX; cntma++) {
      printf("%s         w_ma[%d]: %f\n",prefix,cntma,proc->maWeight[cntma]);
    }

    printf("%s       auto-regressive weights\n",prefix);

    for(cntar=0; cntar<TARMA_NUM_AR_MAX; cntar++) {
      printf("%s         w_ar[%d]: %f\n",prefix,cntar,proc->arWeight[cntar]);
    }

    printf("%s       polynomial factors\n",prefix);

    for(cntpy=0; cntpy<TARMA_NUM_POLY_MAX; cntpy++) {
      printf("%s         w_py[%d]: %f\n",prefix,cntpy,proc->polyWeight[cntpy]);
    }
  }
}


/*
 everything about video modeling which is an extended version of tarma modeling
 */

/*
 this function calculates the packet size (frame size) for each video frame,
  the idt is always fixed to the framerate (i.e., between 20 and 50 fps, default=25)
 input:   - vidoe stream
 output:  - frame size
 */
double tarmaCalculateVideoSample(tarmaVideo_t *video)
{
  double size=0;
  double inputsamples[TARMA_NUM_PROCESSES];
  tarmaProcess_t *proc;
  int frameidx;
  int cntp, cntpy;

  if(video) {
    proc=&(video->tarma_size);
    frameidx=video->tarmaVideoGopStructure[video->tarmaVideoFrameNumber];
    LOG_D(OTG,"TARMA_DEBUG: tarmaCalculateVideoSample(%p) called\n", video);
    LOG_D(OTG,"TARMA_DEBUG:     frameidx=%d\n",frameidx);

    if(frameidx>=0 && frameidx<TARMA_NUM_FRAME_TYPES) {
      for(cntpy=0; cntpy<TARMA_NUM_POLY_MAX; cntpy++) {
        proc->polyWeight[cntpy]=video->polyWeightFrame[frameidx][cntpy];
      }

      for(cntp=0; cntp<TARMA_NUM_PROCESSES; cntp++) {
        inputsamples[cntp]=gaussian_dist(10000,1)-10000;
      }

      size=tarmaCalculateSample(inputsamples,proc);
    }

    /*get ready for the next frame*/
    video->tarmaVideoFrameNumber++;

    if(video->tarmaVideoFrameNumber>=TARMA_NUM_GOP_MAX) {
      video->tarmaVideoFrameNumber=0;
    }

    if(video->tarmaVideoGopStructure[video->tarmaVideoFrameNumber]<0 ||
        video->tarmaVideoGopStructure[video->tarmaVideoFrameNumber]>TARMA_NUM_FRAME_TYPES) {
      video->tarmaVideoFrameNumber=0;
    }
  }

  return size;
}

/*
 this function initializes a video stream (and allocates memory if needed)
 input:   - video stream (or zero if memory should be allocated)
 output:  - the same video stream
 */
tarmaVideo_t *tarmaInitVideo(tarmaVideo_t *video)
{
  tarmaProcess_t *proc;
  int cntp, cntma, cntar, cntpy, cntgop, cnttype;

  LOG_D(OTG,"TARMA_DEBUG: tarmaInitVideo(%p) called\n", video);

  if(video==0) {
    video=(tarmaVideo_t*) malloc(sizeof(tarmaVideo_t));
  }

  proc=&(video->tarma_size);

  for(cntp=0; cntp<TARMA_NUM_PROCESSES; cntp++) {
    proc->inputWeight[cntp]=0;
  }

  for(cntma=0; cntma<TARMA_NUM_MA_MAX; cntma++) {
    proc->maWeight[cntma]=0;
    proc->maHist[cntma]=0;
  }

  for(cntar=0; cntar<TARMA_NUM_AR_MAX; cntar++) {
    proc->arWeight[cntar]=0;
    proc->arHist[cntar]=0;
  }

  for(cntpy=0; cntpy<TARMA_NUM_POLY_MAX; cntpy++) {
    proc->polyWeight[cntpy]=0;
  }

  video->tarmaVideoFrameNumber=0;

  for(cntgop=0; cntgop<TARMA_NUM_GOP_MAX; cntgop++) {
    video->tarmaVideoGopStructure[cntgop]=-1;
  }

  for(cnttype=0; cnttype<TARMA_NUM_FRAME_TYPES; cnttype++) {
    for(cntpy=0; cntpy<TARMA_NUM_POLY_MAX; cntpy++) {
      video->polyWeightFrame[cnttype][cntpy]=0;
    }
  }

  return video;
}

/*
 this function initializes a video stream according to a tarma model
 input:   - video to be initialized
      - compression rate: 1...dvd quality
 output:
 */
void tarmaSetupVideoGop12(tarmaVideo_t *video, double compression)
{
  if(video) {
    video->tarma_size.inputWeight[0]=1;
    video->tarma_size.maWeight[0]=0.47;
    video->tarma_size.maWeight[1]=-0.829;
    video->tarma_size.maWeight[2]=0.358;
    video->tarma_size.arWeight[0]=1;
    video->tarma_size.arWeight[1]=-1.984;
    video->tarma_size.arWeight[2]=0.984;
    video->tarmaVideoGopStructure[0]=0; /*i frame*/
    video->tarmaVideoGopStructure[1]=2; /*b frame*/
    video->tarmaVideoGopStructure[2]=2;
    video->tarmaVideoGopStructure[3]=1; /*p frame*/
    video->tarmaVideoGopStructure[4]=2;
    video->tarmaVideoGopStructure[5]=2;
    video->tarmaVideoGopStructure[6]=1;
    video->tarmaVideoGopStructure[7]=2;
    video->tarmaVideoGopStructure[8]=2;
    video->tarmaVideoGopStructure[9]=1;
    video->tarmaVideoGopStructure[10]=2;
    video->tarmaVideoGopStructure[11]=2;

    if(compression<1) {
      compression=1;
    }

    video->polyWeightFrame[0][0]=55400/compression; /*i frame*/
    video->polyWeightFrame[0][1]=32300/compression;
    video->polyWeightFrame[0][2]=10400/compression;
    video->polyWeightFrame[0][3]=-100/compression;
    video->polyWeightFrame[0][4]=-800/compression;
    video->polyWeightFrame[0][5]=74/compression;
    video->polyWeightFrame[1][0]=20900/compression; /*p frame*/
    video->polyWeightFrame[1][1]=19100/compression;
    video->polyWeightFrame[1][2]=7150/compression;
    video->polyWeightFrame[1][3]=-740/compression;
    video->polyWeightFrame[1][4]=-130/compression;
    video->polyWeightFrame[1][5]=20/compression;
    video->polyWeightFrame[2][0]=11700/compression; /*b frame*/
    video->polyWeightFrame[2][1]=10300/compression;
    video->polyWeightFrame[2][2]=4320/compression;
    video->polyWeightFrame[2][3]=700/compression;
    video->polyWeightFrame[2][4]=-90/compression;
    video->polyWeightFrame[2][5]=-2/compression;
    tarmaPrintVideoInit(video);
  }
}

/*
 this function prints the fixed (weigths) parameters of the input video stream
 input:   - video stream to be printed
 output:
 */
void tarmaPrintVideoInit(tarmaVideo_t *video)
{
  char prefix[]="OTG TARMA DEBUG: ";
  tarmaProcess_t *proc;
  int cntp, cntma, cntar, cntpy, cntgop, cnttype;

  printf("%s tarmaPrintVideoInit(%p) called\n", prefix, video);
  proc=&(video->tarma_size);
  printf("%s      input process weights\n",prefix);

  for(cntp=0; cntp<TARMA_NUM_PROCESSES; cntp++) {
    printf("%s        w[%d]=%f\n",prefix,cntp,proc->inputWeight[cntp]);
  }

  printf("%s      ma weights\n",prefix);

  for(cntma=0; cntma<TARMA_NUM_MA_MAX; cntma++) {
    printf("%s        ma[%d]=%f\n",prefix,cntma,proc->maWeight[cntma]);
  }

  printf("%s      ar weights\n",prefix);

  for(cntar=0; cntar<TARMA_NUM_AR_MAX; cntar++) {
    printf("%s        ar[%d]=%f\n",prefix,cntar,proc->arWeight[cntar]);
  }

  printf("%s      polynomial weights\n",prefix);

  for(cntpy=0; cntpy<TARMA_NUM_POLY_MAX; cntpy++) {
    printf("%s        p[%d]=%f\n",prefix,cntpy,proc->polyWeight[cntpy]);
  }

  printf("%s      video frame number=%d\n",prefix,video->tarmaVideoFrameNumber);
  printf("%s      gop structure\n",prefix);

  for(cntgop=0; cntgop<TARMA_NUM_GOP_MAX; cntgop++) {
    printf("%s        gop[%d]=%d\n",prefix,cntgop,video->tarmaVideoGopStructure[cntgop]);
  }

  printf("%s      frame poly weights\n",prefix);

  for(cntpy=0; cntpy<TARMA_NUM_POLY_MAX; cntpy++) {
    printf("%s      [%d] ",prefix,cntpy);

    for(cnttype=0; cnttype<TARMA_NUM_FRAME_TYPES; cnttype++) {
      printf("     %05.3f",video->polyWeightFrame[cnttype][cntpy]);
    }

    printf("\n");
  }
}





/*
 everything about background traffic modeling according to
 "M. Laner, P. Svoboda, S. Schwarz, M. Rupp, Users in Cells: a Data Traffic Analysis, WCNC'12, Paris, France, 2012".
 */

/*
 this function creates a random realization of the mean data rate of a background-session
 input:
 output:  - random realization of R_s (B/s)
 */
double backgroundRateRnd(void)
{
  double rate;
  rate=pow(10,lognormal_dist(1.3525, 0.1954))/8; /*Byte/s*/;

  if(rate>BACKGROUND_RATE_MAX) {
    rate=BACKGROUND_RATE_MAX;
  } else if(rate<0) {
    rate=0;
  }

  return rate;
}

/*
 this function creates a random realization of the session duration of a background-session
 input:
 output:  - random realization of D_s (ms)
 */
int backgroundSessionDurationRnd(void)
{
  int duration;
  duration = ceil(pow(10,exponential_dist(1/0.3591))*1000); /*ms*/

  if(duration<0) {
    duration=0;
  } else if(duration>1<<30) {
    duration=1<<30;
  }

  return duration;
}

/*
 this function initializes a background stream and allocates memory if needed
 input:   - the bg-stream (or zero if memory shall be allocated)
      - the mean number of users (sessions) to be expected for this stream
 output:  - the same bg-stream
 */
backgroundStream_t *backgroundStreamInit(backgroundStream_t *stream, double lambda_n)
{
  int cnts, numactivenow;

  if(stream==0) {
    stream=(backgroundStream_t*) malloc(sizeof(backgroundStream_t));
  }

  stream->meanNumSessions=lambda_n;
  stream->lastUpdateTime=0;
  numactivenow=poisson_dist(lambda_n); /*how many sessions are active (i.e., started before simulation)*/

  for(cnts=0; cnts<BACKGROUND_NUM_ACTIVE_MAX; cnts++) {
    if(cnts<numactivenow) {
      /*these sessions are already active for unknown time and have therefore a random phaseshift in ds*/
      stream->activeSessions[cnts].meanSessionRate=backgroundRateRnd();
      stream->activeSessions[cnts].endTime=ceil((double)backgroundSessionDurationRnd()*uniform_rng()); /*ms*/
    } else {
      stream->activeSessions[cnts].meanSessionRate=0;
      stream->activeSessions[cnts].endTime=-1;
    }
  }

  LOG_D(OTG,"BACKGROUND_USERS DEBUG: backgroundStreamInit(%p) called\n", stream);
  backgroundPrintStream (stream);
  return stream;
}

/*
 this function updates a given background stream
 input:   - bg-stream
      - current time
 output:
 */
void backgroundUpdateStream(backgroundStream_t *stream, int ctime)
{
  int numNewSessions, cnts, period;

  LOG_D(OTG,"BACKGROUND DEBUG: backgroundUpdateStream(stream*=%p,ctime=%d) called\n", stream, ctime);

  if(stream) {
    period=ctime-stream->lastUpdateTime;
    numNewSessions=poisson_dist(stream->meanNumSessions/5710*period);

    for(cnts=0; cnts<BACKGROUND_NUM_ACTIVE_MAX; cnts++) {
      if(stream->activeSessions[cnts].endTime<ctime) {
        if(numNewSessions>0) {
          stream->activeSessions[cnts].meanSessionRate=backgroundRateRnd();
          stream->activeSessions[cnts].endTime=
            ctime-period*uniform_rng()+backgroundSessionDurationRnd(); /*ms*/
          numNewSessions--;
        } else {
          stream->activeSessions[cnts].meanSessionRate=0;
          stream->activeSessions[cnts].endTime=-1;
        }
      }
    }

    stream->lastUpdateTime=ctime;
    backgroundPrintStream (stream);
  }
}

/*
 this function generates the packet size for a given bg-stream and a given idt,
  the distribution of the idt can thereby be arbitrary (const idt=20ms recommended)
 input:   - bg-stream
        - current time
        - idt of the current packet
 output:  - packet size of the current packet
 */
double backgroundCalculateSize(backgroundStream_t *stream, int ctime, int idt)
{
  int cnts, cntact=0;
  double size=0;
  double mrate=0;

  backgroundUpdateStream(stream, ctime);
  LOG_D(OTG,"BACKGROUND DEBUG: backgroundCalculateSize(stream*=%p,idt=%d,ctime=%d) called\n", stream, idt, ctime);

  if(stream) {
    for(cnts=0; cnts<BACKGROUND_NUM_ACTIVE_MAX; cnts++) {
      if(stream->activeSessions[cnts].endTime>ctime) {
        mrate+=stream->activeSessions[cnts].meanSessionRate;
        cntact++;
      }
    }

    size=mrate*idt/1000;
    LOG_D(OTG,"BACKGROUND DEBUG:     cntact=%02d, idt=%05d, agg_mrate=%05.1f, size=%04.1f\n", cntact, idt, mrate,size);
  }

  return size;
}

/*
 this function prints the current status of the bg-stream
 input:   - bg-stream
 output:
 */
void backgroundPrintStream(backgroundStream_t *stream)
{
  int cnts;

  LOG_D(OTG,"BACKGROUND DEBUG: backgroundPrintStream(%p)\n", stream);

  if(stream) {
    LOG_D(OTG,"BACKGROUND DEBUG:     meanNumSessions(lambda_n)=%f\n",stream->meanNumSessions);

    for(cnts=0; cnts<BACKGROUND_NUM_ACTIVE_MAX; cnts++) {
      LOG_D(OTG,"BACKGROUND DEBUG:       session[%d] -> mrate=%06.3f, etime=%05d\n",
            cnts, stream->activeSessions[cnts].meanSessionRate, stream->activeSessions[cnts].endTime);
    }
  }
}