/*****************************************************************************\
 *
 * SoPerfGraph.cpp
 *
 * Performance graph node
 *
 * Authors: PCJohn (peciva AT fit.vutbr.cz)
 * Contributors:
 *
 * ----------------------------------------------------------------------------
 *
 * THIS SOFTWARE IS NOT COPYRIGHTED
 *
 * This source code is offered for use in the public domain.
 * You may use, modify or distribute it freely.
 *
 * This source code is distributed in the hope that it will be useful but
 * WITHOUT ANY WARRANTY.  ALL WARRANTIES, EXPRESS OR IMPLIED ARE HEREBY
 * DISCLAIMED.  This includes but is not limited to warranties of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * If you find the source code useful, authors will kindly welcome
 * if you give them credit and keep their names with their source code,
 * but do not feel forced to do so.
 *
\*****************************************************************************/


#include <Inventor/nodes/SoAnnotation.h>
#include <Inventor/nodes/SoBaseColor.h>
#include <Inventor/nodes/SoComplexity.h>
#include <Inventor/nodes/SoCoordinate3.h>
#include <Inventor/nodes/SoFont.h>
#include <Inventor/nodes/SoIndexedTriangleStripSet.h>
#include <Inventor/nodes/SoLightModel.h>
#include <Inventor/nodes/SoMaterial.h>
#include <Inventor/nodes/SoOrthographicCamera.h>
#include <Inventor/nodes/SoText2.h>
#include <Inventor/nodes/SoTexture2.h>
#include <Inventor/nodes/SoTextureCoordinate2.h>
#include <Inventor/nodes/SoTranslation.h>
#include <Inventor/nodes/SoTransparencyType.h>
#include <Inventor/actions/SoGLRenderAction.h>
#include <Inventor/actions/SoGetBoundingBoxAction.h>
#include <Inventor/actions/SoPickAction.h>
#include <Inventor/sensors/SoNodeSensor.h>
#include <Inventor/misc/SoChildList.h>
#include <Inventor/SbTime.h>
#include <Inventor/C/tidbits.h> // coin_next_power_of_two
#include "SoPerfGraph.h"



SO_NODE_SOURCE(SoPerfGraph);


#define THIS this->pimpl
#define TEXTURE_COMPONENTS  4
#define NUM_VALUES_OVER  20
#define AVG_UPDATE_TIME  0.35
#define AVG_SUM_TIME  1.



class SoPerfGraphP {
public:
  float *values;
  double *timeStamps;
  int numValues;
  int startIndex;
  float maxValue;
  float renderedMax;

  int texRequestedWidth;
  int texRequestedHeight;
  float maxTexCoordX;
  float maxTexCoordY;
  int texIndex;

  SoChildList children;
  SoPerfGraph *parent;

  SoOrthographicCamera *camera;
  SoCoordinate3 *coordNode;
  SoIndexedTriangleStripSet *triStrip;
  SoTexture2 *textureNode;
  SoTextureCoordinate2 *texCoordNode;
  SoTranslation *textTranslationMaxNode;
  SoTranslation *textTranslationAvgNode;
  SoText2 *textNodeMax;
  SoText2 *textNodeAvg;
  double lastAvgUpdate;

  SbViewportRegion previousViewport;
  SoNodeSensor nodeSensor;
  struct objData {
    SbBool vpValid : 1;
    SbBool posValid : 1;
    SbBool sizeValid : 1;
    SbBool textureNeedsResize : 1;
    objData() : vpValid(FALSE), posValid(FALSE), sizeValid(FALSE), textureNeedsResize(TRUE)  {}
  } objData;

  SoPerfGraphP(SoPerfGraph *aparent) : values(NULL), timeStamps(NULL),
      children(aparent), parent(aparent), lastAvgUpdate(0.)
  {
    maxTexCoordX = 1.f; // this value may be used BEFORE its proper initialization in updateSceneIfNecessary()
    nodeSensor.setFunction(fieldChangedCB);
    nodeSensor.setData(this);
    nodeSensor.setPriority(0); 
    nodeSensor.attach(aparent); 
  }

  ~SoPerfGraphP()  { delete[] values; delete[] timeStamps; }

  static void fieldChangedCB(void *data, SoSensor *sensor);
  void updateSceneIfNecessary(const SbViewportRegion &vp, SbBool textureDataRequired);
  void updateTextureCoordinates(int texSizeY);
  void redrawTexture();
  static float roundUp(float value);
};



static int32_t coordIndices[] = { 0,1,2,3,-1, };
static SbVec2f texCoords[] = { SbVec2f(0.f,0.f), SbVec2f(0.f,1.f), SbVec2f(1.f,0.f), SbVec2f(1.f,1.f) };



SoPerfGraph::SoPerfGraph()
{
  SO_NODE_CONSTRUCTOR(SoPerfGraph);

  SO_NODE_ADD_FIELD(vertAlignment,    (BOTTOM));
  SO_NODE_ADD_FIELD(horAlignment,     (LEFT));
  SO_NODE_ADD_FIELD(size,             (0.35f,0.15f));
  SO_NODE_ADD_FIELD(position,         (0.03f,0.03f));
  SO_NODE_ADD_FIELD(sizeInPixels,     (0,0));
  SO_NODE_ADD_FIELD(positionInPixels, (0,0));
  SO_NODE_ADD_FIELD(vertScreenOrigin, (BOTTOM));
  SO_NODE_ADD_FIELD(horScreenOrigin,  (LEFT));
  SO_NODE_ADD_FIELD(dataScale,        (2.f,1.f));

  SO_NODE_DEFINE_ENUM_VALUE(VertAlignment, BOTTOM);
  SO_NODE_DEFINE_ENUM_VALUE(VertAlignment, HALF);
  SO_NODE_DEFINE_ENUM_VALUE(VertAlignment, TOP);
  SO_NODE_SET_SF_ENUM_TYPE(vertAlignment, VertAlignment);
  SO_NODE_SET_SF_ENUM_TYPE(vertScreenOrigin, VertAlignment);

  SO_NODE_DEFINE_ENUM_VALUE(HorAlignment, LEFT);
  SO_NODE_DEFINE_ENUM_VALUE(HorAlignment, CENTER);
  SO_NODE_DEFINE_ENUM_VALUE(HorAlignment, RIGHT);
  SO_NODE_SET_SF_ENUM_TYPE(horAlignment, HorAlignment);
  SO_NODE_SET_SF_ENUM_TYPE(horScreenOrigin, HorAlignment);

  pimpl = new SoPerfGraphP(this);

  THIS->numValues = 128 + NUM_VALUES_OVER;
  THIS->values = new float[THIS->numValues];
  THIS->timeStamps = new double[THIS->numValues];
  memset(THIS->values, 0, THIS->numValues*sizeof(float));
  memset(THIS->timeStamps, 0, THIS->numValues*sizeof(double));
  THIS->maxValue = 0.f;
  THIS->renderedMax = 0.f;

  THIS->startIndex = 0;
  THIS->texIndex = 0;

  SoSeparator *root = new SoAnnotation;
  root->renderCaching = SoSeparator::OFF;

  THIS->camera = new SoOrthographicCamera;
  THIS->camera->height = 1.f;
  THIS->camera->viewportMapping = SoCamera::LEAVE_ALONE;
  root->addChild(THIS->camera);

  SoLightModel *lm = new SoLightModel;
  lm->model.setValue(SoLightModel::BASE_COLOR);
  root->addChild(lm);

  SoTransparencyType *tt = new SoTransparencyType;
  tt->value = SoTransparencyType::BLEND;
  root->addChild(tt);

  THIS->coordNode = new SoCoordinate3;
  THIS->coordNode->point.setNum(4);
  root->addChild(THIS->coordNode);

  THIS->texCoordNode = new SoTextureCoordinate2;
  THIS->texCoordNode->point.setValues(0, sizeof(texCoords)/sizeof(int32_t), texCoords);
  root->addChild(THIS->texCoordNode);

  SoComplexity *cplx = new SoComplexity;
  cplx->textureQuality = 0.1f; // this should disable texture filtering,
                               // that we do not want to happen in our graph (causes distortion)
  cplx->type.setIgnored(TRUE);
  cplx->value.setIgnored(TRUE);
  root->addChild(cplx);

  THIS->textureNode = new SoTexture2;
  THIS->textureNode->model = SoTexture2::REPLACE;
  root->addChild(THIS->textureNode);

  THIS->triStrip = new SoIndexedTriangleStripSet;
  THIS->triStrip->coordIndex.setValues(0, sizeof(coordIndices)/sizeof(int32_t), coordIndices);
  root->addChild(THIS->triStrip);

  // set default font
  SoFont *font = new SoFont;
  root->addChild(font);

  root->addChild(new SoTexture2);

  THIS->textTranslationMaxNode = new SoTranslation;
  root->addChild(THIS->textTranslationMaxNode);

  THIS->textNodeMax = new SoText2;
  root->addChild(THIS->textNodeMax);

  THIS->textTranslationAvgNode = new SoTranslation;
  root->addChild(THIS->textTranslationAvgNode);

  THIS->textNodeAvg = new SoText2;
  THIS->textNodeAvg->justification.setValue(SoText2::RIGHT);
  THIS->textNodeAvg->string.setValue("12");
  root->addChild(THIS->textNodeAvg);

  THIS->children.append(root);
}


SoPerfGraph::~SoPerfGraph()
{
  delete pimpl;
}


void SoPerfGraphP::fieldChangedCB(void *data, SoSensor *sensor)
{
  SoPerfGraphP *p = (SoPerfGraphP*)data;
  if (((SoNodeSensor*)sensor)->getTriggerNode() != p->parent)
    return;

  SoField *f = ((SoNodeSensor*)sensor)->getTriggerField();

  // Camera settings may need to be changed
  if (f == &p->parent->vertScreenOrigin || f == &p->parent->horScreenOrigin)
    p->objData.vpValid = FALSE;

  // Coordinates may need recalculation
  if (f == &p->parent->vertAlignment || f == &p->parent->horAlignment ||
      f == &p->parent->position || f == &p->parent->positionInPixels)
    p->objData.posValid = FALSE;

  // Texture size and coordinates may need to be updated
  if (f == &p->parent->size || f == &p->parent->sizeInPixels ||
      f == &p->parent->dataScale)
    p->objData.sizeValid = FALSE;
}


void SoPerfGraph::appendValue(float v)
{
  // update maxValue
  float newMax = THIS->renderedMax;
  if (v > THIS->maxValue) {
    // incoming value is the greatest one
    THIS->maxValue = v;
    newMax = THIS->roundUp(THIS->maxValue);
  } else {
    // leaving value is the greatest one
    if (THIS->values[THIS->startIndex] == THIS->maxValue) {
      float max = v;
      THIS->values[THIS->startIndex] = v; // old value, that we certainly do not want to find
      int i,c = THIS->numValues;
      for (i=0; i<c; i++)
        if (THIS->values[i] > max)
          max = THIS->values[i];
      THIS->maxValue = max;
      newMax = THIS->roundUp(THIS->maxValue);
    }
  }
  // update text
  if (newMax != THIS->renderedMax) {
    
    // postphone scaling down (avoids quick rescalling)
    if (newMax < THIS->renderedMax)
      newMax = THIS->roundUp(THIS->maxValue * 1.1f);

    // update text and texture
    if (newMax != THIS->renderedMax) {
      THIS->renderedMax = newMax;
      THIS->redrawTexture();
      SbString s;
      s.sprintf("%.5g", newMax);
      THIS->textNodeMax->string.setValue(s);
    }
  }

  // update value array
  THIS->values[THIS->startIndex] = v;
  THIS->timeStamps[THIS->startIndex] = SbTime::getTimeOfDay().getValue();

  // update avg value
  // The update is done four times per second from values up to one second old.
  if (THIS->lastAvgUpdate + AVG_UPDATE_TIME <= THIS->timeStamps[THIS->startIndex]) {
    THIS->lastAvgUpdate = THIS->timeStamps[THIS->startIndex];
    double stop = THIS->timeStamps[THIS->startIndex] - AVG_SUM_TIME;
    float sum = THIS->values[THIS->startIndex];
    int num = 1;
    int i = THIS->startIndex - 1;
    if (i<0)  i += THIS->numValues;
    while (THIS->timeStamps[i] > stop && i != THIS->startIndex) {
      sum += THIS->values[THIS->startIndex];
      num++;
      if (--i<0)  i += THIS->numValues;
    }
    SbString s;
    s.sprintf("avg: %#.5g", sum/num);
    THIS->textNodeAvg->string.setValue(s);
  }

  // move to the next index
  THIS->startIndex++;
  if (THIS->startIndex == THIS->numValues)
    THIS->startIndex = 0;

  // update texture
  SbVec2s texSize;
  int components;
  unsigned char *img = THIS->textureNode->image.startEditing(texSize, components);
  if (img) {
    rasterizeColumn(img+THIS->texIndex*texSize[0]*TEXTURE_COMPONENTS, texSize[0], v);

    THIS->texIndex++;
    if (THIS->texIndex == texSize[1])
      THIS->texIndex = 0;
  }
  THIS->textureNode->image.finishEditing();

  // animate texture coordinates
  THIS->updateTextureCoordinates(texSize[1]);
}


void SoPerfGraphP::updateSceneIfNecessary(const SbViewportRegion &vp, SbBool textureDataRequired)
{
  if (!(vp == previousViewport) || !objData.posValid || !objData.sizeValid) {

    objData.posValid = TRUE;
    objData.sizeValid = TRUE;

    SbVec2s winSize = vp.getWindowSize();

    if (winSize[0] == 0)  winSize[0] = 1;
    if (winSize[1] == 0)  winSize[1] = 1;

    float pixSizeX = 1.f / winSize[0];
    float pixSizeY = 1.f / winSize[1];

    float realSizeX = parent->size.getValue()[0] + parent->sizeInPixels.getValue()[0] * pixSizeX;
    float realSizeY = parent->size.getValue()[1] + parent->sizeInPixels.getValue()[1] * pixSizeY;
    
    float adX,adY;
    switch (parent->horAlignment.getValue()) {
    case SoPerfGraph::LEFT:   adX = 0.f; break;
    case SoPerfGraph::CENTER: adX = -realSizeX / 2.f; break;
    case SoPerfGraph::RIGHT:  
    default:                  adX = -realSizeX; break;
    }
    switch (parent->vertAlignment.getValue()) {
    case SoPerfGraph::TOP:    adY = -realSizeY; break;
    case SoPerfGraph::HALF:   adY = -realSizeY / 2.f; break;
    case SoPerfGraph::BOTTOM:
    default:                  adY = 0.f; break;
    }

    float x1 = parent->position.getValue()[0] + (parent->positionInPixels.getValue()[0] * pixSizeX) + adX;
    float y1 = parent->position.getValue()[1] + (parent->positionInPixels.getValue()[1] * pixSizeY) + adY;
    float x2 = x1 + realSizeX;
    float y2 = y1 + realSizeY;

    // update coordinates
    SbVec3f *c = coordNode->point.startEditing();
    c[0].setValue(x1, y1, -5.f);
    c[1].setValue(x2, y1, -5.f);
    c[2].setValue(x1, y2, -5.f);
    c[3].setValue(x2, y2, -5.f);
    coordNode->point.finishEditing();

    // update text position
    //
    // please note: The second translation have to "undone" the first one.
    //
    // another note: Shift by half of pixel was introduced by experiments 
    // with inconsistencies between rendered box corners and text positions.
    // When resizing, text did not stayed 2 pixels from the corner but jumped between two and three.
    //
    textTranslationMaxNode->translation.setValue(x1+1.5f*pixSizeX,        y2-11.5f*pixSizeY,  -4.f);
    textTranslationAvgNode->translation.setValue(x2-3.0f*pixSizeX-x1, y1-(y2-13.0f*pixSizeY), -3.f); 

    int minimalTexWidth  = int(realSizeX / pixSizeX / parent->dataScale.getValue()[0] + 0.5);
    int minimalTexHeight = int(realSizeY / pixSizeY / parent->dataScale.getValue()[1] + 0.5);
    texRequestedWidth  = (minimalTexWidth  <= 1) ? 1 : coin_next_power_of_two(minimalTexWidth-1);
    texRequestedHeight = (minimalTexHeight <= 1) ? 1 : coin_next_power_of_two(minimalTexHeight-1);
    SbVec2s currentTexSize;
    int tmp;
    textureNode->image.getValue(currentTexSize, tmp);
    if (currentTexSize[0] != texRequestedHeight || currentTexSize[1] != texRequestedWidth) {
      objData.textureNeedsResize = TRUE;
      maxTexCoordX = float(minimalTexWidth)  / texRequestedWidth;
      maxTexCoordY = float(minimalTexHeight) / texRequestedHeight;
    }
  }

  if (!(vp == previousViewport) || !objData.vpValid) {

    objData.vpValid = TRUE;

    float cx,cy;
    switch (parent->horScreenOrigin.getValue()) {
    case SoPerfGraph::LEFT:   cx = 0.5f; break;
    case SoPerfGraph::CENTER: cx = 0.f; break;
    case SoPerfGraph::RIGHT: 
    default:                  cx = -0.5f; break;
    }
    switch (parent->vertScreenOrigin.getValue()) {
    case SoPerfGraph::TOP:    cy = -0.5f; break;
    case SoPerfGraph::HALF:   cy = 0.f; break;
    case SoPerfGraph::BOTTOM:
    default:                  cy = 0.5f; break;
    }
    camera->position.setValue(cx, cy, 1.f);

    previousViewport = vp;
  }

  if (textureDataRequired && objData.textureNeedsResize) {
    objData.textureNeedsResize = FALSE;
    
    SbVec2s currentTexSize;
    int tmp;
    textureNode->image.getValue(currentTexSize, tmp);
    if (currentTexSize[0] != texRequestedHeight || currentTexSize[1] != texRequestedWidth) {

      // resize values array
      int newNumValues = int(texRequestedWidth * maxTexCoordX + 0.5f) + NUM_VALUES_OVER;
      int newIndex = 0;
      float *newValues = new float[newNumValues];
      double *newTimeStamps = new double[newNumValues];
      int delta = newNumValues - numValues;
      
      // fill beginning with zeros
      while (newIndex<delta) {
        newValues    [newIndex] = 0.f;
        newTimeStamps[newIndex] = 0.f;
        newIndex++;
      }

      // move data "after the pointer"
      int moveNum = numValues - startIndex;
      if (delta < 0)
        moveNum += delta; // value is negative => using "+="
      if (moveNum > 0) {
        memcpy(&newValues    [newIndex], &values    [numValues-moveNum], moveNum*sizeof(float));
        memcpy(&newTimeStamps[newIndex], &timeStamps[numValues-moveNum], moveNum*sizeof(double));
        newIndex += moveNum;
      }

      // move data "before the pointer"
      if (moveNum < 0)
        moveNum += startIndex;
      else
        moveNum = startIndex;
      memcpy(&newValues    [newIndex], &values    [0], moveNum*sizeof(float));
      memcpy(&newTimeStamps[newIndex], &timeStamps[0], moveNum*sizeof(double));
      newIndex += moveNum;
      if (newIndex == newNumValues)
        newIndex = 0;
      assert(newIndex < newNumValues && "Bad index.");

      // assign new values
      delete[] values;
      delete[] timeStamps;
      values = newValues;
      timeStamps = newTimeStamps;
      startIndex = newIndex;
      numValues = newNumValues;

      // resize texture
      textureNode->image.setValue(SbVec2s(texRequestedHeight, texRequestedWidth), TEXTURE_COMPONENTS, NULL);

      // update texture coordinates and texture
      redrawTexture();
      updateTextureCoordinates(texRequestedWidth);
    }
  }
}


void SoPerfGraph::rasterizeColumn(unsigned char *buf, int size, const float v)
{
  int n = int(v / THIS->renderedMax * size);

  int x;
  for(x=0; x<n && x<size; x++) {
    *(buf+0) = 0;
    *(buf+1) = 128;
    *(buf+2) = 255;
    *(buf+3) = 255;
    buf += TEXTURE_COMPONENTS;
  }
  for(; x<size; x++) {
    *(buf+0) = 0;
    *(buf+1) = 0;
    *(buf+2) = 128;
    *(buf+3) = 128;
    buf += TEXTURE_COMPONENTS;
  }
}


void SoPerfGraphP::redrawTexture()
{
  // start texture editing
  SbVec2s texSize;
  int components;
  unsigned char *img = textureNode->image.startEditing(texSize, components);
  if (img) {
#ifndef NDEBUG
    unsigned char *base = img;
#endif
    int c = int(maxTexCoordX * texSize[1] +0.5f);
    assert(c <= texSize[1]);
    texIndex = c;
    if (texIndex == texSize[1])
      texIndex = 0;
  
    // edit texture
    int i = startIndex - c;
    if (i<0)  i+=numValues;
    while (--c >= 0) {
      parent->rasterizeColumn(img, texSize[0], values[i]);
      img += texSize[0]*TEXTURE_COMPONENTS;
      if (++i==numValues)  i=0;
    }
    assert(i==startIndex && "Final index is not equal startIndex.");
    assert(img-base <= texSize[0]*texSize[1]*TEXTURE_COMPONENTS);
  }
  textureNode->image.finishEditing();
}


void SoPerfGraphP::updateTextureCoordinates(int texSizeY)
{
  SbVec2f *tc = texCoordNode->point.startEditing();
  float tcRight = float(texIndex) / texSizeY;
  float tcLeft = tcRight - maxTexCoordX;
  tc[0][1] = tcLeft;
  tc[2][1] = tcLeft;
  tc[1][1] = tcRight;
  tc[3][1] = tcRight;
  texCoordNode->point.finishEditing();
}


float SoPerfGraphP::roundUp(float value)
{
  // handle negative values
  float sign = (value>=0.f) ? +1.f : -1.f;
  value *= sign;

  // handle zero and inf
  if (value == 0.f)
    return 1.f;
  if (value / 10.f == value)
    return value;

  // compute exponent
  float e = 1.;
  if (value > 1.f)
    while (value > 10.f) {
      value /= 10.f;
      e *= 10.f;
    }
  else
    while (value < 1.f) {
      value *= 10.f;
      e /= 10.f;
    }

  // truncate number
  int i = int(value);
  if (float(i) != value)
    i++;
  
  // round up
  int r;
  if (i>10) r=20; else
  if (i> 6) r=10; else
  if (i> 4) r= 6; else
  if (i> 2) r= 4; else
  { assert(i>=1); r=2; }

  // return value
  return sign*r*e;
}


void SoPerfGraph::GLRender(SoGLRenderAction *action)
{
  THIS->updateSceneIfNecessary(action->getViewportRegion(), TRUE);
  SoPerfGraph::doAction(action);
}


void SoPerfGraph::getBoundingBox(SoGetBoundingBoxAction *action)
{
  THIS->updateSceneIfNecessary(action->getViewportRegion(), FALSE);
  SoPerfGraph::doAction((SoAction*)action);
}


void SoPerfGraph::pick(SoPickAction *action)
{
#if 0 // Enabling picking can cause some weird behaviour,
      // therefore it is disabled for SoPerfGraph.
      //
      // The reason is that the scene graph contains camera and
      // there is known limitation in Inventor design, that
      // cameras does not have elements, that will be able to restore
      // original camera state after traversing of SoPerfGraph is done.
      // During the rendering, everything seems fine since modelview stack is used,
      // but during the raypick action, orthographic projection of SoPerfGraph
      // stays for the rest of the scene (e.g. camera state is not restored when
      // traversal is leaving SoPerfGraph separator).
  THIS->updateSceneIfNecessary(action->getViewportRegion(), FALSE);
  SoPerfGraph::doAction((SoAction*)action);
#endif
}


void SoPerfGraph::doAction(SoAction *action)
{
  THIS->children.traverse(action);
}


void SoPerfGraph::initClass()
{
  SO_NODE_INIT_CLASS(SoPerfGraph, SoNode, "Node");
}


void SoPerfGraph::cleanup()
{
  SoPerfGraph::atexit_cleanup();
}


void SoPerfGraph::callback(SoCallbackAction *action)
{
  SoPerfGraph::doAction((SoAction*)action);
}


void SoPerfGraph::getMatrix(SoGetMatrixAction *action)
{
  SoPerfGraph::doAction((SoAction*)action);
}


void SoPerfGraph::handleEvent(SoHandleEventAction *action)
{
  SoPerfGraph::doAction((SoAction*)action);
}


void SoPerfGraph::getPrimitiveCount(SoGetPrimitiveCountAction *action)
{
  SoPerfGraph::doAction((SoAction*)action);
}


void SoPerfGraph::audioRender(SoAudioRenderAction *action)
{
  SoPerfGraph::doAction((SoAction*)action);
}


SoChildList* SoPerfGraph::getChildren() const
{
  return &THIS->children;
}
