// main file...

#include <stdio.h>
#include <string.h>
#include <time.h>
#include <math.h>

#include "int64type.h"
#include "stats.h"

#include "gd.h"

/////////////////////////////////////////////////////////////////////
// #defines
/////////////////////////////////////////////////////////////////////

#define NDEBUG

#define kStaticStringSize 1024

// assumed sampling interval (in minutes)
#define kAssumedSamplingInterval 5

// (points) per hour, per day, per week, per [year]
// - 112896 records
// - 56 weeks of 12 datapoints per hour (every 5 minutes)
#define kNumDatapoints (12 * 24 * 7 * 4 * 56)

// graph constants
#define kGraphNumMinutes        240
#define kGraphNumHours           96
#define kGraphNumDays            48
#define kGraphNumWeeks           24
#define kGraphNumMonths          12

#define kGraphWidth             960
#define kGraphHeight            150

#define kGraphYTicks             10
#define kGraphScaleElementWidth  20

#define kGraphNameMinutes       "minutes"
#define kGraphNameHours         "hours"
#define kGraphNameDays          "days"
#define kGraphNameWeeks         "weeks"
#define kGraphNameMonths        "months"

/////////////////////////////////////////////////////////////////////
// structs
/////////////////////////////////////////////////////////////////////

// struct to hold time, rxbytes, txbytes triplet
struct sDatapoint {
  time_t mIntervalStart;
  time_t mIntervalEnd;
  int64type mRxBytes;
  int64type mTxBytes;
};

class cInternalData {
  public:
    sDatapoint* mArray;
    int mArraySize;
    int mArrayUsed;

    int mUnitDivisor;
    int mGraphNumUnits;
    int mColumnInterval;

    int mInterval, mIntervalPerColumn;
    char mIntervalUnits[kStaticStringSize];

    char mGraphFilename[kStaticStringSize];
    char mScaleElementFilename[kStaticStringSize];
    char mImagemapFilename[kStaticStringSize];
    char mImagemapName[kStaticStringSize];
  
    double mScaling;
    sDatapoint mAverage;
    sDatapoint m50Percentile;
    sDatapoint m95Percentile;
    sDatapoint mMax;
    sDatapoint m50PercentileSum;
    sDatapoint m95PercentileSum;
    sDatapoint mTotal;

    int mNumColumns;
    int* mColumnWidth;
    sDatapoint* mColumnAverage;
    sDatapoint* mColumnMax;
    sDatapoint* mColumn50PercentileSum;
    sDatapoint* mColumn95PercentileSum;
    sDatapoint* mColumnTotal;

    cInternalData() {
      mArraySize = 0;
      mNumColumns = 0;
    }

    ~cInternalData() {
      if (mArraySize) delete mArray;
      if (mNumColumns) {
        delete mColumnWidth;
        delete mColumnAverage;
        delete mColumnMax;
        delete mColumn50PercentileSum;
        delete mColumn95PercentileSum;
        delete mColumnTotal;
      }
    }

    void allocateArray(int pArraySize) {
      mArraySize = pArraySize;
      mArray = new sDatapoint[mArraySize];
    }

    void allocateColumnStats(int pNumColumns) {
      mNumColumns = pNumColumns;
      mColumnWidth = new int[mNumColumns];
      mColumnAverage = new sDatapoint[mNumColumns];
      mColumnMax = new sDatapoint[mNumColumns];
      mColumn50PercentileSum = new sDatapoint[mNumColumns];
      mColumn95PercentileSum = new sDatapoint[mNumColumns];
      mColumnTotal = new sDatapoint[mNumColumns];
    }

};

/////////////////////////////////////////////////////////////////////
// globals
/////////////////////////////////////////////////////////////////////

// array of datapoints
// - re-aligned such that the last element read is at the beginning of the array
sDatapoint theDatapoints[kNumDatapoints];

// number of records read from file (may be greater than kNumDatapoints)
int theNumRecords;

// this is the element id of the current image being out
// - element id is always incremented...
int theElementID = 0;

/////////////////////////////////////////////////////////////////////
// function prototypes
/////////////////////////////////////////////////////////////////////

bool  assignArgumentToString(int argc, char** argv, int pArgNumber, char* pDest, int pMaxLength, const char* pDefault);
void  parseFile(FILE* pFile);
void  generateGraphs(const char* pOutputPath);
void  loadArrayFromDatapoints(cInternalData &pData);
void  generateGraph(cInternalData &pData, const char* pPath, const char* pName);
void  graphDrawLevelLine(gdImagePtr &pGD, int64type pValue, int64type pMax, int pColor);
void  graphDrawLevelTriangle(gdImagePtr &pGD, int64type pValue, int64type pMax, int pColor);
void  generateScaleElementImage(FILE* pOutFile);
void  htmlHeader(FILE* pOutFile);
void  htmlFooter(FILE* pOutFile);
void  htmlGraphLine(FILE* pOutFile, const cInternalData &pData, bool pIncludeImagemap);
void  htmlIntervalSummary(FILE* pOutFile, const cInternalData &pData);
void  htmlImagemapEntry(FILE* pOutFile, int pX1, int pX2, const sDatapoint& pDatapoint);
char* renderNumberBytes(char* pBuffer, double pValue, const char* pFmtString = 0);

/////////////////////////////////////////////////////////////////////
// main
/////////////////////////////////////////////////////////////////////

int main(int argc, char** argv) {
  // open file, read file, close file
  char theInputFileName[kStaticStringSize];
  char theOutputPath[kStaticStringSize];

  // grab cmd line argument
  assignArgumentToString(argc, argv, 1, theInputFileName, kStaticStringSize, 0);
  assignArgumentToString(argc, argv, 2, theOutputPath, kStaticStringSize, "./");

  // open file
  FILE* theInput;
  if (theInputFileName[0]) {
    theInput = fopen(theInputFileName, "rt");

    // die if input not open properly
    if (!theInput) {
      return 0;
    }

  } else {
    theInput = stdin;
  }

  // populate arrays
  parseFile(theInput);
  generateGraphs(theOutputPath);

  // close file if necessary
  if (theInput != stdin) {
    fclose(theInput);
  }

  return 0;
}

/////////////////////////////////////////////////////////////////////
// supporting functions
/////////////////////////////////////////////////////////////////////

// assignArgumentToString
// - argc and argv passed in from main
// - assign argument pArgNum into pDest with proper string length checks
//   or copies in pDefault if pArgNumber doesn't exist
// - return true if assigned
bool assignArgumentToString(int argc, char** argv, int pArgNumber, char* pDest, int pMaxLength, const char* pDefault) {
  const char* theArg;
  int theLen;
  
  if (argc > pArgNumber) {
    theArg = argv[pArgNumber];
    theLen = strlen(theArg);
  } else {
    theArg = pDefault;
    if (theArg) {
      theLen = strlen(theArg);
    } else {
      pDest[0] = 0;
      return false;
    }
  }
    
  strncpy(pDest, theArg, pMaxLength);
  if (theLen >= pMaxLength) {
    pDest[pMaxLength-1] = 0;
  }

  return true;
}

// parseFile
// - file format expected:
//   <int:time>, <rx bytes>, <tx bytes>
// - note that rx bytes, and tx bytes, may be greater than int32
// - dumb parser: assume that all records are kAssumedSamplingInterval minutes apart already (don't interpolate)
void parseFile(FILE* pFile) {
  // store a local copy of the array
  sDatapoint* theTempDatapoints = new sDatapoint[kNumDatapoints];

  // every time we parse, we zero the number of records
  theNumRecords = 0;

  // - and clear the global datapoint array
  memset(theDatapoints, 0, kNumDatapoints * sizeof(sDatapoint));

  // create format strings (based on compiler)
  char theSScanfFmtStr[kStaticStringSize];
  char thePrintfFmtStr[kStaticStringSize];

  sprintf(theSScanfFmtStr, "%s, %s, %s", "%d", int64fmtspec, int64fmtspec);
  sprintf(thePrintfFmtStr, "%s, %s, %s\n", "%d", int64fmtspec, int64fmtspec);
  
  // parse
  sDatapoint theLastDatapoint;
  theLastDatapoint.mIntervalEnd = -1;
  while(!feof(pFile)) {
    char theBuffer[kStaticStringSize];
    char* theFGetStr;

    int theTime;
    int64type theRxBytes;
    int64type theTxBytes;

    theFGetStr = fgets(theBuffer, kStaticStringSize, pFile);
    if (!theFGetStr) {
      break;
    }

    int theNumAssigned;
    theNumAssigned = sscanf(theBuffer, theSScanfFmtStr, &theTime, &theRxBytes, &theTxBytes);
    if (theNumAssigned != 3) {
      break;
    }

    // now we have a triplet of time, rxbytes, and txbytes
    // - do further processing on those values
    int theDeltaTime = theTime - theLastDatapoint.mIntervalEnd;
    int theDeltaTimeMinutes = (int)(((double)theDeltaTime / 60.0) + 0.5);

    // trap parse errors
    if (theDeltaTimeMinutes <= 0) {
      continue;
    }

    int64type theRxDelta = theRxBytes - theLastDatapoint.mRxBytes;
    int64type theTxDelta = theTxBytes - theLastDatapoint.mTxBytes;

    //int64type theRxDeltaPerTime = theRxDelta / theDeltaTimeMinutes;
    //int64type theTxDeltaPerTime = theTxDelta / theDeltaTimeMinutes;

    if (theRxDelta < 0) {
      theRxDelta = (theRxBytes / theDeltaTimeMinutes) * kAssumedSamplingInterval;
    }
    if (theTxDelta < 0) {
      theTxDelta = (theTxBytes / theDeltaTimeMinutes) * kAssumedSamplingInterval;
    }

    int theIndex = theNumRecords % kNumDatapoints;
    theTempDatapoints[theIndex].mIntervalStart = theLastDatapoint.mIntervalEnd;
    theTempDatapoints[theIndex].mIntervalEnd = theTime;
    theTempDatapoints[theIndex].mRxBytes = theRxDelta;
    theTempDatapoints[theIndex].mTxBytes = theTxDelta;

#ifndef NDEBUG
    printf(thePrintfFmtStr, theTime, theRxBytes, theTxBytes);
#endif

    // iterative

    // - since we're taking differences, discard the first element
    if (theLastDatapoint.mIntervalEnd > 0) {
      theNumRecords++;
    }

    // save last datapoint
    theLastDatapoint.mIntervalEnd = theTime;
    theLastDatapoint.mRxBytes = theRxBytes;
    theLastDatapoint.mTxBytes = theTxBytes;
  }

  // realign array
  int theNumToCopy = (theNumRecords <= kNumDatapoints) ? theNumRecords : kNumDatapoints;
  for (int i = 0; i < theNumToCopy; i++) {
    theDatapoints[i] = theTempDatapoints[((theNumRecords - 1) - i) % kNumDatapoints];
  }

#ifndef NDEBUG
  for (int j = 0; j < theNumRecords; j++) {
    int theTime = theDatapoints[j].mIntervalEnd;
    int64type theRxBytes = theDatapoints[j].mRxBytes;
    int64type theTxBytes = theDatapoints[j].mTxBytes;

    printf(thePrintfFmtStr, theTime, theRxBytes, theTxBytes);
  }
#endif

  // cleanup
  delete theTempDatapoints;
}

// generateGraphs
// - generate 5 graphs using the gd library
// - unit, number of columns, column width
//   - minutes 240 *  4 = 960
//   - hours    96 * 10 = 960 (60 minute period)
//   - days     24 * 40 = 960 (24 hour period)
//   - weeks    16 * 60 = 960 (7 day period)
//   - months   12 * 80 = 960 (30 day period)
void generateGraphs(const char* pOutputPath) {
  // calculate divisor values
  int theMinuteDivisor = 1;
  int theHourDivisor   = theMinuteDivisor * (60 / kAssumedSamplingInterval);
  int theDayDivisor    = theHourDivisor * 24;
  int theWeekDivisor   = theDayDivisor * 7;
  int theMonthDivisor  = theDayDivisor * 30;

  cInternalData theMinuteData;
  cInternalData theHourData;
  cInternalData theDayData;
  cInternalData theWeekData;
  cInternalData theMonthData;

  theMinuteData.allocateArray(kGraphNumMinutes);
  theMinuteData.mUnitDivisor = theMinuteDivisor;
  theMinuteData.mColumnInterval = (60 / kAssumedSamplingInterval);
  theMinuteData.mInterval = kGraphNumMinutes;
  theMinuteData.mIntervalPerColumn = kAssumedSamplingInterval;
  strcpy(theMinuteData.mIntervalUnits, "minutes");

  theHourData.allocateArray(kGraphNumHours);
  theHourData.mUnitDivisor = theHourDivisor;
  theHourData.mColumnInterval = 24;
  theHourData.mInterval = kGraphNumHours;
  theHourData.mIntervalPerColumn = 1;
  strcpy(theHourData.mIntervalUnits, "hours");

  theDayData.allocateArray(kGraphNumDays);
  theDayData.mUnitDivisor = theDayDivisor;
  theDayData.mColumnInterval = 7;
  theDayData.mInterval = kGraphNumDays;
  theDayData.mIntervalPerColumn = 1;
  strcpy(theDayData.mIntervalUnits, "days");

  theWeekData.allocateArray(kGraphNumWeeks);
  theWeekData.mUnitDivisor = theWeekDivisor;
  theWeekData.mColumnInterval = 4;
  theWeekData.mInterval = kGraphNumWeeks;
  theWeekData.mIntervalPerColumn = 1;
  strcpy(theWeekData.mIntervalUnits, "weeks");

  theMonthData.allocateArray(kGraphNumMonths);
  theMonthData.mUnitDivisor = theMonthDivisor;
  theMonthData.mColumnInterval = 3;
  theMonthData.mInterval = kGraphNumMonths;
  theMonthData.mIntervalPerColumn = 1;
  strcpy(theMonthData.mIntervalUnits, "months");


  // load arrays
  loadArrayFromDatapoints(theMinuteData);
  loadArrayFromDatapoints(theHourData);
  loadArrayFromDatapoints(theDayData);
  loadArrayFromDatapoints(theWeekData);
  loadArrayFromDatapoints(theMonthData);

  // generate graphs
  generateGraph(theMinuteData, pOutputPath, kGraphNameMinutes);
  generateGraph(theHourData,   pOutputPath, kGraphNameHours);
  generateGraph(theDayData,    pOutputPath, kGraphNameDays);
  generateGraph(theWeekData,   pOutputPath, kGraphNameWeeks);
  generateGraph(theMonthData,  pOutputPath, kGraphNameMonths);

  // generate index file
  char theFilename[kStaticStringSize];
  strcpy(theFilename, pOutputPath);
  if (pOutputPath[strlen(pOutputPath - 1)] != '/') {
    strcat(theFilename, "/");
  }
  strcat(theFilename, "index.html");

  // open file for output
  FILE* theOutput;
  theOutput = fopen(theFilename, "wt");

  // dump contents to file
  htmlHeader(theOutput);

  htmlGraphLine(theOutput, theMinuteData, false);
  htmlGraphLine(theOutput, theHourData, false);
  htmlGraphLine(theOutput, theDayData, false);
  htmlGraphLine(theOutput, theWeekData, false);
  htmlGraphLine(theOutput, theMonthData, false);

  htmlFooter(theOutput);

  // close output file
  fclose(theOutput);
}

// loadArrayFromDatapoints
// - load pArray from the global datapoint array
// - aggregate pUnitDivisor steps into each pArray point up to pGraphNumUnits points
// - remember the number of points we've copied
void loadArrayFromDatapoints(cInternalData &pData) {
  sDatapoint* theArray = pData.mArray;
  int theArraySize = pData.mArraySize;
  int theUnitDivisor = pData.mUnitDivisor;
  
  int theColumnInterval = pData.mColumnInterval;
  int theNumColumns = (int)ceil((double)theArraySize / (double)theColumnInterval);
  int theColumnWidth = kGraphWidth / theArraySize;
 
  // clear memory (so we can just dump this data into the graphs later...)
  memset(theArray, 0, theArraySize * sizeof(sDatapoint));

  // how many units are we going to copy into?
  int theMaxUnits = ((theNumRecords / theUnitDivisor) < theArraySize) ? (theNumRecords / theUnitDivisor) : theArraySize;

  CStats<int64type> theRxStats(theMaxUnits);
  CStats<int64type> theTxStats(theMaxUnits);
  
  // keep column stats
  pData.allocateColumnStats(theNumColumns);
  int theColumnIndex = -1;
  int theColumnNumElements = -1;
  
  { // zero out all columns
    for (int i = 0; i < theNumColumns; i++) {
      pData.mColumnAverage[i].mRxBytes = 0;
      pData.mColumn50PercentileSum[i].mRxBytes = 0;
      pData.mColumn95PercentileSum[i].mRxBytes = 0;
      pData.mColumnMax[i].mRxBytes = 0;
      pData.mColumnTotal[i].mRxBytes = 0;

      pData.mColumnAverage[i].mTxBytes = 0;
      pData.mColumn50PercentileSum[i].mTxBytes = 0;
      pData.mColumn95PercentileSum[i].mTxBytes = 0;
      pData.mColumnMax[i].mTxBytes = 0;
      pData.mColumnTotal[i].mTxBytes = 0;
    }
  }

  CStats<int64type> theColumnRxStats(theColumnInterval * theUnitDivisor);
  CStats<int64type> theColumnTxStats(theColumnInterval * theUnitDivisor);

  CStats<int64type> theColumnAccRxStats(theColumnInterval);
  CStats<int64type> theColumnAccTxStats(theColumnInterval);

  // copy loop
  for (int i = 0; i < theMaxUnits; i++) {
    sDatapoint theAcc;
    theAcc.mIntervalEnd = theDatapoints[i * theUnitDivisor].mIntervalEnd;
    theAcc.mRxBytes = 0;
    theAcc.mTxBytes = 0;
    
    // re-initialize column stats
    if ((i % theColumnInterval) == 0) {
      theColumnIndex++;
      theColumnNumElements = ((theArraySize - i) > theColumnInterval) ? theColumnInterval : (theArraySize - i);

      theColumnRxStats.clear();
      theColumnTxStats.clear();

      theColumnAccRxStats.clear();
      theColumnAccTxStats.clear();

      pData.mColumnTotal[theColumnIndex].mIntervalEnd = theArray[i].mIntervalEnd;

    } else {
      theColumnNumElements--;
    }

    // aggregate
    for (int j = 0; j < theUnitDivisor; j++) {
      int theIndex = (i * theUnitDivisor) + j;

      int64type theRxBytes = theDatapoints[theIndex].mRxBytes;
      int64type theTxBytes = theDatapoints[theIndex].mTxBytes;

      theAcc.mRxBytes += theRxBytes;
      theAcc.mTxBytes += theTxBytes;
      theAcc.mIntervalStart = theDatapoints[theIndex].mIntervalStart;

      // collect column stats
      theColumnRxStats.insert(theRxBytes);
      theColumnTxStats.insert(theTxBytes);
    }

    // copy back
    theArray[i] = theAcc;

    theColumnAccRxStats.insert(theAcc.mRxBytes);
    theColumnAccTxStats.insert(theAcc.mTxBytes);

    theRxStats.insert(theAcc.mRxBytes);
    theTxStats.insert(theAcc.mTxBytes);

    // store column stats
    if ((theColumnNumElements == 1) || (i == (theMaxUnits - 1))) {
      pData.mColumnTotal[theColumnIndex].mIntervalStart = theArray[i].mIntervalStart;

      pData.mColumnAverage[theColumnIndex].mRxBytes = theColumnAccRxStats.average();
      pData.mColumnMax[theColumnIndex].mRxBytes = theColumnAccRxStats.max();
      pData.mColumn50PercentileSum[theColumnIndex].mRxBytes = theColumnRxStats.sumToPercentile(0.50);
      pData.mColumn95PercentileSum[theColumnIndex].mRxBytes = theColumnRxStats.sumToPercentile(0.95);
      pData.mColumnTotal[theColumnIndex].mRxBytes = theColumnAccRxStats.total();

      pData.mColumnAverage[theColumnIndex].mTxBytes = theColumnAccTxStats.average();
      pData.mColumnMax[theColumnIndex].mTxBytes = theColumnAccTxStats.max();
      pData.mColumn50PercentileSum[theColumnIndex].mTxBytes = theColumnTxStats.sumToPercentile(0.50);
      pData.mColumn95PercentileSum[theColumnIndex].mTxBytes = theColumnTxStats.sumToPercentile(0.95);
      pData.mColumnTotal[theColumnIndex].mTxBytes = theColumnAccTxStats.total();
    }
  }

  { // just the column widths
    theColumnIndex = -1;

    for (int i = 0; i < theArraySize; i++) {
      // re-initialize column stats
      if ((i % theColumnInterval) == 0) {
        theColumnIndex++;
        theColumnNumElements = ((theArraySize - i) > theColumnInterval) ? theColumnInterval : (theArraySize - i);
        pData.mColumnWidth[theColumnIndex] = theColumnNumElements * theColumnWidth;
      }
    }
  }

  // collect overall stats
  pData.mAverage.mRxBytes = theRxStats.average();
  pData.m50Percentile.mRxBytes = theRxStats.percentile(0.50);
  pData.m95Percentile.mRxBytes = theRxStats.percentile(0.95);
  pData.mMax.mRxBytes = theRxStats.max();
  pData.m50PercentileSum.mRxBytes = theRxStats.sumToPercentile(0.50);
  pData.m95PercentileSum.mRxBytes = theRxStats.sumToPercentile(0.95);
  pData.mTotal.mRxBytes = theRxStats.total();

  pData.mAverage.mTxBytes = theTxStats.average();
  pData.m50Percentile.mTxBytes = theTxStats.percentile(0.50);
  pData.m95Percentile.mTxBytes = theTxStats.percentile(0.95);
  pData.mMax.mTxBytes = theTxStats.max();
  pData.m50PercentileSum.mTxBytes = theTxStats.sumToPercentile(0.50);
  pData.m95PercentileSum.mTxBytes = theTxStats.sumToPercentile(0.95);
  pData.mTotal.mTxBytes = theTxStats.total();

  pData.mArrayUsed = theMaxUnits;
}

// generateGraph
// - filename will be copied into pData.mFilename
// - generate a graph name pName.<ext> on pPath from pData.mArray of size pData.mArraySize
// - column widths are automatically determined
//   - every column interval, change the background color
// - also generate imagemap file (filename in pData.mImagemapFilename)
void generateGraph(cInternalData &pData, const char* pPath, const char* pName) {
  sDatapoint* theArray = pData.mArray;
  int theArraySize = pData.mArraySize;
  int theColumnInterval = pData.mColumnInterval;
  
  // files
  strcpy(pData.mGraphFilename, pName);
  strcat(pData.mGraphFilename, ".png");

  strcpy(pData.mScaleElementFilename, pName);
  strcat(pData.mScaleElementFilename, "_se.png");

  strcpy(pData.mImagemapFilename, pName);
  strcat(pData.mImagemapFilename, "_im.html");

  strcpy(pData.mImagemapName, pName);
  strcat(pData.mImagemapName, "_im");

  char theFilename[kStaticStringSize];
  char theScaleElementFilename[kStaticStringSize];
  char theImagemapFilename[kStaticStringSize];

  strcpy(theFilename, pPath);
  strcpy(theScaleElementFilename, pPath);
  strcpy(theImagemapFilename, pPath);
  if (pPath[strlen(pPath - 1)] != '/') {
    strcat(theFilename, "/");
    strcat(theScaleElementFilename, "/");
    strcat(theImagemapFilename, "/");
  }
  strcat(theFilename, pData.mGraphFilename);
  strcat(theScaleElementFilename, pData.mScaleElementFilename);
  strcat(theImagemapFilename, pData.mImagemapFilename);

  FILE *theOutput, *theScaleElementOutput, *theImagemapOutput;

  theOutput = fopen(theFilename, "wb");
  theScaleElementOutput = fopen(theScaleElementFilename, "wb");
  theImagemapOutput = fopen(theImagemapFilename, "wb");

  int64type theMaxRx = pData.mMax.mRxBytes;
  int64type theMaxTx = pData.mMax.mTxBytes;

  int64type theMax = (theMaxRx > theMaxTx) ? theMaxRx : theMaxTx;
  pData.mScaling = theMax;

  // sanitize maximum because it is used as a divisor
  if (theMax == 0) {
    theMax = 1;
  }

  // prepare imagemap file
  htmlHeader(theImagemapOutput);
  htmlGraphLine(theImagemapOutput, pData, true);

  fprintf(theImagemapOutput, "<map name=\"%s\">\n", pData.mImagemapName);

  // graphics stuff
  gdImagePtr theGDIm;

  // allocate image
  theGDIm = gdImageCreate(kGraphWidth, kGraphHeight);

  // allocate colours
  int theGDWhite         = gdImageColorAllocate(theGDIm, 255, 255, 255);
  int theGDBlack         = gdImageColorAllocate(theGDIm, 0,   0,   0);
  int theGDLightGray     = gdImageColorAllocate(theGDIm, 224, 224, 224);
  int theGDVeryLightGray = gdImageColorAllocate(theGDIm, 236, 236, 236);
  int theGDRed           = gdImageColorAllocate(theGDIm, 255, 0,   0);
  int theGDGreen         = gdImageColorAllocate(theGDIm, 0,   255, 0);
  int theGDDampRed       = gdImageColorAllocate(theGDIm, 224, 0,   0);
  int theGDDampGreen     = gdImageColorAllocate(theGDIm, 0,   224, 0);
  int theGDLightRed      = gdImageColorAllocate(theGDIm, 255, 192, 192);
  int theGDLightGreen    = gdImageColorAllocate(theGDIm, 192, 255, 192);
  int theGDMediumRed     = gdImageColorAllocate(theGDIm, 160,  64, 64);
  int theGDMediumGreen   = gdImageColorAllocate(theGDIm, 64,  160, 64);
  int theGDDarkRed       = gdImageColorAllocate(theGDIm, 128, 0,   0);
  int theGDDarkGreen     = gdImageColorAllocate(theGDIm, 0,   128, 0);

  // get rid of compiler warning...
  // - to use these colors, we can't set them to zero
  theGDBlack = 0;
  theGDDarkRed = 0;
  theGDDarkGreen = 0;

  int theGridColor = theGDLightGray;
  int theColumnColor = theGDVeryLightGray;

  int theRxLineColor = theGDDampRed;
  int theTxLineColor = theGDDampGreen;

  int theRx95thLineColor = theGDRed;
  int theTx95thLineColor = theGDGreen;

  int theRxAverageColor = theGDMediumRed;
  int theTxAverageColor = theGDMediumGreen;

  int theRx95PercentileColor = theGDLightRed;
  int theTx95PercentileColor = theGDLightGreen;

  // transparent background
  gdImageColorTransparent(theGDIm, theGDWhite);

  // populate image

  int theColumnWidth = kGraphWidth / theArraySize;

  // background columns
  int theNumColumns = (int)ceil((double)theArraySize / (double)theColumnInterval);
  {
    for (int i = 0; i < theNumColumns; i++) {
      if (i % 2) {
        int theX1, theX2;
        theX1 = kGraphWidth - ((i + 1) * theColumnInterval * theColumnWidth);
        theX2 = kGraphWidth - ((i + 0) * theColumnInterval * theColumnWidth);

        gdImageFilledRectangle(theGDIm, theX1, 0, theX2, kGraphHeight, theColumnColor);
      }
    }
  }

  // grid
  {
    for (int i = 0; i < kGraphYTicks; i++) {
      int theHeight = (kGraphHeight / kGraphYTicks) * i;
      gdImageLine(theGDIm, 0, theHeight, kGraphWidth, theHeight, theGridColor);
    }
  }

  // average
  int64type theRxAverage = pData.mAverage.mRxBytes;
  int64type theTxAverage = pData.mAverage.mTxBytes;

  int64type theRx95Percentile = pData.m95Percentile.mRxBytes;
  int64type theTx95Percentile = pData.m95Percentile.mTxBytes;

  graphDrawLevelLine(theGDIm, theRxAverage, theMax, theRxAverageColor);
  graphDrawLevelLine(theGDIm, theTxAverage, theMax, theTxAverageColor);

  graphDrawLevelLine(theGDIm, theRx95Percentile, theMax, theRx95PercentileColor);
  graphDrawLevelLine(theGDIm, theTx95Percentile, theMax, theTx95PercentileColor);

  int theRx95thHeight = (int)(((double)theRx95Percentile / (double)theMax) * kGraphHeight);
  int theTx95thHeight = (int)(((double)theTx95Percentile / (double)theMax) * kGraphHeight);

  for (int i = 0; i < theArraySize; i++) {
    // data elements
    int64type theRx = theArray[i].mRxBytes;
    int64type theTx = theArray[i].mTxBytes;

    // render data element
    int theRxHeight = (int)(((double)theRx / (double)theMax) * kGraphHeight);
    int theTxHeight = (int)(((double)theTx / (double)theMax) * kGraphHeight);

    int theRxX1, theRxX2;
    int theTxX1, theTxX2;

    theRxX1 = (theArraySize - i - 1) * theColumnWidth;
    theRxX2 = theRxX1 + (theColumnWidth / 2) - 1;
    theTxX1 = theRxX2 + 1;
    theTxX2 = theTxX1 + (theColumnWidth / 2) - 1;

    int theRxColor;
    int theTxColor;

    if (theRxHeight > theRx95thHeight) {
      theRxColor = theRx95thLineColor;
    } else {
      theRxColor = theRxLineColor;
    }

    if (theTxHeight > theTx95thHeight) {
      theTxColor = theTx95thLineColor;
    } else {
      theTxColor = theTxLineColor;
    }

    gdImageFilledRectangle(theGDIm, theRxX1, kGraphHeight - theRxHeight, theRxX2, kGraphHeight, theRxColor);
    gdImageFilledRectangle(theGDIm, theTxX1, kGraphHeight - theTxHeight, theTxX2, kGraphHeight, theTxColor);

    // output imagemap entry
    htmlImagemapEntry(theImagemapOutput, theRxX1, theTxX2, theArray[i]);
  }

  // pointers
  graphDrawLevelTriangle(theGDIm, theRxAverage, theMax, theRxAverageColor);
  graphDrawLevelTriangle(theGDIm, theTxAverage, theMax, theTxAverageColor);

  graphDrawLevelTriangle(theGDIm, theRx95Percentile, theMax, theRx95PercentileColor);
  graphDrawLevelTriangle(theGDIm, theTx95Percentile, theMax, theTx95PercentileColor);

  // output
  gdImagePng(theGDIm, theOutput);

  // scale element image
  generateScaleElementImage(theScaleElementOutput);

  // done imagemap output
  fprintf(theImagemapOutput, "</map>\n");
  htmlFooter(theImagemapOutput);

  // close files
  fclose(theOutput);
  fclose(theScaleElementOutput);
  fclose(theImagemapOutput);

  // free image
  gdImageDestroy(theGDIm);
}

void  graphDrawLevelLine(gdImagePtr &pGD, int64type pValue, int64type pMax, int pColor) {
  int theHeight = kGraphHeight - (int)(((double)pValue / (double)pMax) * kGraphHeight);
  gdImageLine(pGD, 0, theHeight, kGraphWidth, theHeight, pColor);
}

void  graphDrawLevelTriangle(gdImagePtr &pGD, int64type pValue, int64type pMax, int pColor) {
  int theHeight = kGraphHeight - (int)(((double)pValue / (double)pMax) * kGraphHeight);
  gdPoint theTriangle[3];

  theTriangle[0].x = 0;
  theTriangle[0].y = theHeight + 5;
  theTriangle[1].x = 20;
  theTriangle[1].y = theHeight;
  theTriangle[2].x = 0;
  theTriangle[2].y = theHeight - 5;

  gdImageFilledPolygon(pGD, theTriangle, 3, pColor);
}

void generateScaleElementImage(FILE* pOutFile) {
  gdImagePtr theGDIm;

  // allocate image
  int theScaleElementHeight = kGraphHeight / kGraphYTicks;
  theGDIm = gdImageCreate(kGraphScaleElementWidth, theScaleElementHeight);

  // allocate colours
  int theGDWhite         = gdImageColorAllocate(theGDIm, 255, 255, 255);
  int theGDDarkGray      = gdImageColorAllocate(theGDIm, 128, 128, 128);
  int theGDLightGray     = gdImageColorAllocate(theGDIm, 224, 224, 224);
  int theGDVeryLightGray = gdImageColorAllocate(theGDIm, 236, 236, 236);

  // transparent background
  gdImageColorTransparent(theGDIm, theGDWhite);

  // draw box
  gdImageFilledRectangle(theGDIm, 2, 0, kGraphScaleElementWidth - 6, 6, theGDVeryLightGray);
  gdImageFilledRectangle(theGDIm, 3, 0, kGraphScaleElementWidth - 4, 4, theGDLightGray);
  gdImageFilledRectangle(theGDIm, 4, 0, kGraphScaleElementWidth - 2, 2, theGDDarkGray);

  // output
  gdImagePng(theGDIm, pOutFile);

  // free image
  gdImageDestroy(theGDIm);
}

// htmlHeader
// - output html header
void  htmlHeader(FILE* pOutFile) {
  time_t theTime = time(0);
  tm* theTM;
  theTM = localtime(&theTime);

  fprintf(pOutFile, "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n");
  fprintf(pOutFile, "<html>\n");
  fprintf(pOutFile, "<head>\n");
  fprintf(pOutFile, "<title>interface stats</title>\n");
  fprintf(pOutFile, "<script language=\"javascript\" type=\"text/javascript\">\n");
  fprintf(pOutFile, "function c() { window.status=''; return true; }\n");
  fprintf(pOutFile, "function s(pID) { theElement=document.getElementById(pID); if (theElement) { window.status=theElement.title; } else { window.status=''; } return true; }\n");
  fprintf(pOutFile, "</script>\n");
  fprintf(pOutFile, "</head>\n");
  fprintf(pOutFile, "<body>\n");
  fprintf(pOutFile, "<h1>interface stats - last updated: %s</h1>\n", asctime(theTM));
  fprintf(pOutFile, "<hr />\n");
}

// htmlFooter
// - output html footer
void  htmlFooter(FILE* pOutFile) {
  fprintf(pOutFile, "<p><a href=\"http://www.herethere.net/ifusage\"><i>the ifusage project</i></a></p>");
  fprintf(pOutFile, "</body>\n");
  fprintf(pOutFile, "</html>\n");
}

// htmlGraphLine
// - output to pOutFile
// - pData.mInterval * pData.mIntervalPerColumn number of pData.mIntervalUnits
// - Scaling, RxAverage, RxTotal, TxAverage, TxTotal
// - graph image filename pData.mImageFileName
// - if pIncludeImagemap, then don't href to imagemap file, instead set ismap to imagemap
void htmlGraphLine(FILE* pOutFile, const cInternalData &pData, bool pIncludeImagemap) {
  int theInterval = pData.mInterval;
  int theIntervalPerColumn = pData.mIntervalPerColumn;
  const char* theIntervalUnits = pData.mIntervalUnits;

  double theScaling = pData.mScaling;
  int theNumColumns = pData.mNumColumns;

  const char* theGraphFilename = pData.mGraphFilename;
  const char* theScaleElementFilename = pData.mScaleElementFilename;
  const char* theImagemapFilename = pData.mImagemapFilename;

  char theScaleElementRB[kStaticStringSize];
  char theScaleElementTitleBuffer[kStaticStringSize];

  htmlIntervalSummary(pOutFile, pData);

  fprintf(pOutFile, "<table border=0 cellpadding=0 cellspacing=0 style=\"border-collapse: collapse\" summary=\"graph group\">\n");
  {
    char theTitleBuffer[kStaticStringSize];

    sprintf(theTitleBuffer, "%d %s", theInterval * theIntervalPerColumn, theIntervalUnits);
    fprintf(pOutFile, "<tr><td><img alt=\"se\" title=\"%s\" border=0 src=\"%s\" /></td>", theTitleBuffer, theScaleElementFilename);

    for (int i = 0; i < theNumColumns; i++) {
      int theIndex = theNumColumns - 1 - i;

      int64type theInterval = pData.mColumnTotal[theIndex].mIntervalEnd - pData.mColumnTotal[theIndex].mIntervalStart;

      int theWidth = pData.mColumnWidth[theIndex];

      double theRxAverage = (double)pData.mColumnAverage[theIndex].mRxBytes;
      double theRxMax = (double)pData.mColumnMax[theIndex].mRxBytes;
      double theRx95PercentileSum = (double)pData.mColumn95PercentileSum[theIndex].mRxBytes;
      double theRxTotal = (double)pData.mColumnTotal[theIndex].mRxBytes;
      double theRxAverageBitrate = theRxAverage / (double)theInterval;
      double theRxMaxBitrate = theRxMax / (double)theInterval;

      double theTxAverage = (double)pData.mColumnAverage[theIndex].mTxBytes;
      double theTxMax = (double)pData.mColumnMax[theIndex].mTxBytes;
      double theTx95PercentileSum = (double)pData.mColumn95PercentileSum[theIndex].mTxBytes;
      double theTxTotal = (double)pData.mColumnTotal[theIndex].mTxBytes;
      double theTxAverageBitrate = theTxAverage / (double)theInterval;
      double theTxMaxBitrate = theTxMax / (double)theInterval;

      char theRxAverageRB[kStaticStringSize];
      char theRxMaxRB[kStaticStringSize];
      char theRx95PercentileRB[kStaticStringSize];
      char theRxTotalRB[kStaticStringSize];

      char theTxAverageRB[kStaticStringSize];
      char theTxMaxRB[kStaticStringSize];
      char theTx95PercentileRB[kStaticStringSize];
      char theTxTotalRB[kStaticStringSize];

      char theRxAverageBitrateBuffer[kStaticStringSize];
      char theRxMaxBitrateBuffer[kStaticStringSize];
      char theTxAverageBitrateBuffer[kStaticStringSize];
      char theTxMaxBitrateBuffer[kStaticStringSize];

      if (theInterval > 0) {
        sprintf(theRxAverageBitrateBuffer, " (%s/s)", renderNumberBytes(theRxAverageRB, theRxAverageBitrate));
        sprintf(theRxMaxBitrateBuffer, " (%s/s)", renderNumberBytes(theRxMaxRB, theRxMaxBitrate));
        sprintf(theTxAverageBitrateBuffer, " (%s/s)", renderNumberBytes(theTxAverageRB, theTxAverageBitrate));
        sprintf(theTxMaxBitrateBuffer, " (%s/s)", renderNumberBytes(theTxMaxRB, theTxMaxBitrate));
      } else {
        theRxAverageBitrateBuffer[0] = 0;
        theRxMaxBitrateBuffer[0] = 0;
        theTxAverageBitrateBuffer[0] = 0;
        theTxMaxBitrateBuffer[0] = 0;
      }

      sprintf(theScaleElementTitleBuffer, "rx average=%s%s; max=%s%s; sum(95th%%)=%s; total=%s",
        renderNumberBytes(theRxAverageRB, theRxAverage), theRxAverageBitrateBuffer,
        renderNumberBytes(theRxMaxRB, theRxMax), theRxMaxBitrateBuffer,
        renderNumberBytes(theRx95PercentileRB, theRx95PercentileSum),
        renderNumberBytes(theRxTotalRB, theRxTotal));
      theElementID++;
      fprintf(pOutFile, "<td><img alt=\"se\" id=\"i%d\" border=0 title=\"%s\" src=\"%s\" width=%d height=%d onmouseout=\"return c()\" onmouseover=\"return s('i%d')\" /></td>",
        theElementID, theScaleElementTitleBuffer, theScaleElementFilename, theWidth / 2, kGraphHeight / kGraphYTicks, theElementID);

      sprintf(theScaleElementTitleBuffer, "tx average=%s%s; max=%s%s; sum(95th%%)=%s; total=%s",
        renderNumberBytes(theTxAverageRB, theTxAverage), theTxAverageBitrateBuffer,
        renderNumberBytes(theTxMaxRB, theTxMax), theTxMaxBitrateBuffer,
        renderNumberBytes(theTx95PercentileRB, theTx95PercentileSum),
        renderNumberBytes(theTxTotalRB, theTxTotal));
      theElementID++;
      fprintf(pOutFile, "<td><img alt=\"se\" id=\"i%d\" border=0 title=\"%s\" src=\"%s\" width=%d height=%d onmouseout=\"return c()\" onmouseover=\"return s('i%d')\" /></td>",
        theElementID, theScaleElementTitleBuffer, theScaleElementFilename, theWidth / 2, kGraphHeight / kGraphYTicks, theElementID);
    }
    
    fprintf(pOutFile, "</tr>");
  }
  
  sprintf(theScaleElementTitleBuffer, "%s", renderNumberBytes(theScaleElementRB, theScaling));

  theElementID++;
  fprintf(pOutFile, "<tr><td><img alt=\"se\" id=\"i%d\" border=0 title=\"%s\" src=\"%s\" onmouseout=\"return c()\" onmouseover=\"return s('i%d')\" /></td>", theElementID, theScaleElementTitleBuffer, theScaleElementFilename, theElementID);
  
  char theImgTagBuffer[kStaticStringSize];
  if (pIncludeImagemap) {
    const char* theImagemapName = pData.mImagemapName;
    sprintf(theImgTagBuffer, "<img alt=\"se\" src=\"%s\" border=0 usemap=\"#%s\" ismap />", theGraphFilename, theImagemapName);
  } else {
    sprintf(theImgTagBuffer, "<a href=\"%s\"><img alt=\"\" border=0 src=\"%s\" /></a>", theImagemapFilename, theGraphFilename);
  }
  
  fprintf(pOutFile, "<td colspan=%d rowspan=%d>%s</td></tr>", theNumColumns * 2, kGraphYTicks, theImgTagBuffer);

  {
    for (int i = kGraphYTicks - 1; i >= 1; i--) {
      sprintf(theScaleElementTitleBuffer, "%s", renderNumberBytes(theScaleElementRB, theScaling * ((double)i / (double)kGraphYTicks)));
      theElementID++;
      fprintf(pOutFile, "<tr><td><img alt=\"se\" id=\"i%d\" border=0 title=\"%s\" src=\"%s\" onmouseout=\"return c()\" onmouseover=\"return s('i%d')\" /></td></tr>", theElementID, theScaleElementTitleBuffer, theScaleElementFilename, theElementID);
    }
  }
  
  fprintf(pOutFile, "</table>\n");
}

// htmlIntervalSummary
// - output interval summary according to pData
void  htmlIntervalSummary(FILE* pOutFile, const cInternalData &pData) {
  int theInterval = pData.mInterval;
  int theIntervalPerColumn = pData.mIntervalPerColumn;
  const char* theIntervalUnits = pData.mIntervalUnits;

  double theScaling = pData.mScaling;

  double theRxAverage = pData.mAverage.mRxBytes;
  double theRx95Percentile = pData.m95Percentile.mRxBytes;
  double theRx95PercentileSum = pData.m95PercentileSum.mRxBytes;
  double theRxTotal = pData.mTotal.mRxBytes;

  double theTxAverage = pData.mAverage.mTxBytes;
  double theTx95Percentile = pData.m95Percentile.mTxBytes;
  double theTx95PercentileSum = pData.m95PercentileSum.mTxBytes;
  double theTxTotal = pData.mTotal.mTxBytes;

  char theScalingRB[kStaticStringSize];

  char theRxAverageRB[kStaticStringSize];
  char theRx95PercentileRB[kStaticStringSize];
  char theRx95PercentileSumRB[kStaticStringSize];
  char theRxTotalRB[kStaticStringSize];

  char theTxAverageRB[kStaticStringSize];
  char theTx95PercentileRB[kStaticStringSize];
  char theTx95PercentileSumRB[kStaticStringSize];
  char theTxTotalRB[kStaticStringSize];

  fprintf(pOutFile, "<p>interval: %d %s", theInterval * theIntervalPerColumn, theIntervalUnits);
  if (theIntervalPerColumn > 1) {
    fprintf(pOutFile, " (%d per column)", theIntervalPerColumn);
  }

  fprintf(pOutFile, "; scaling: <b>%s</b><br /><font color=\"#808080\">[average / 95th%%]</font>, [sum(95th%%) / total]:",
    renderNumberBytes(theScalingRB, theScaling));
  fprintf(pOutFile, " <font color=\"#ff0000\">rx</font> <font color=\"#808080\">[ %s / %s ]</font>, [ %s / %s ],",
    renderNumberBytes(theRxAverageRB, theRxAverage),
    renderNumberBytes(theRx95PercentileRB, theRx95Percentile),
    renderNumberBytes(theRx95PercentileSumRB, theRx95PercentileSum),
    renderNumberBytes(theRxTotalRB, theRxTotal));
  fprintf(pOutFile, " <font color=\"#00ff00\">tx</font> <font color=\"#808080\">[ %s / %s ]</font>, [ %s / %s ]</p>\n",
    renderNumberBytes(theTxAverageRB, theTxAverage),
    renderNumberBytes(theTx95PercentileRB, theTx95Percentile),
    renderNumberBytes(theTx95PercentileSumRB, theTx95PercentileSum),
    renderNumberBytes(theTxTotalRB, theTxTotal));
}

// htmlImagemapEntry
// - output to pOutFile an imagemap entry from pX1 to pX2 with the datapoint
//   contained in pDatapoint
void  htmlImagemapEntry(FILE* pOutFile, int pX1, int pX2, const sDatapoint& pDatapoint) {
  char theTitleText[kStaticStringSize];

  char theTimeBeforeBuffer[kStaticStringSize];
  char theTimeCurrentBuffer[kStaticStringSize];

  char theRxBitrateBuffer[kStaticStringSize];
  char theTxBitrateBuffer[kStaticStringSize];

  char theIntervalBuffer[kStaticStringSize];

  char theRxRB[kStaticStringSize];
  char theTxRB[kStaticStringSize];

  tm theTMNow;
  tm theTMBefore;
  tm theTMCurrent;

  time_t theTimeNow = time(0);
  time_t theTimeBefore = pDatapoint.mIntervalStart;
  time_t theTimeCurrent = pDatapoint.mIntervalEnd;

  theTMNow = *localtime(&theTimeNow);

  theTMBefore = *localtime(&theTimeBefore);
  strcpy(theTimeBeforeBuffer, asctime(&theTMBefore));
  theTimeBeforeBuffer[strlen(theTimeBeforeBuffer) - 1] = 0;

  theTMCurrent = *localtime(&theTimeCurrent);
  strcpy(theTimeCurrentBuffer, asctime(&theTMCurrent));
  theTimeCurrentBuffer[strlen(theTimeCurrentBuffer) - 1] = 0;

  char* theTimeBeforePtr = theTimeBeforeBuffer;
  char* theTimeCurrentPtr = theTimeCurrentBuffer;

  if ((theTimeBefore > 0) && (theTimeCurrent > 0) && (theTimeCurrent > theTimeBefore)) {
    // calculate bitrates
    int theInterval = theTimeCurrent - theTimeBefore; // interval in seconds
    double theRxBitrate = (double)pDatapoint.mRxBytes / (double)theInterval;
    double theTxBitrate = (double)pDatapoint.mTxBytes / (double)theInterval;

    sprintf(theRxBitrateBuffer, " (%s/s)", renderNumberBytes(theRxRB, theRxBitrate));
    sprintf(theTxBitrateBuffer, " (%s/s)", renderNumberBytes(theTxRB, theTxBitrate));
    
    // suppress extra date information

    // suppress similarites between before and now
    if (theTMNow.tm_year == theTMBefore.tm_year) {
      if ((theTMBefore.tm_mon == theTMNow.tm_mon) && (theTMBefore.tm_mday == theTMNow.tm_mday)) {
        // suppress month and date
        theTimeBeforePtr += 11;
      }

      // suppress year
      theTimeBeforePtr[strlen(theTimeBeforePtr) - 5] = 0;
    }

    // suppress similarities between before and current
    if (theTMBefore.tm_year == theTMCurrent.tm_year) {
      if ((theTMBefore.tm_mon == theTMCurrent.tm_mon) && (theTMBefore.tm_mday == theTMCurrent.tm_mday)) {
        // suppress month and date
        theTimeCurrentPtr += 11;
      }

      // suppress year
      theTimeCurrentPtr[strlen(theTimeCurrentPtr) - 5] = 0;
    }

    sprintf(theIntervalBuffer, " at interval from %s until %s", theTimeBeforePtr, theTimeCurrentPtr);
  } else {
    theRxBitrateBuffer[0] = 0;
    theTxBitrateBuffer[0] = 0;
    theIntervalBuffer[0] = 0;
  }

  sprintf(theTitleText, "rx: %s%s, tx: %s%s%s",
    renderNumberBytes(theRxRB, (double)pDatapoint.mRxBytes), theRxBitrateBuffer,
    renderNumberBytes(theTxRB, (double)pDatapoint.mTxBytes), theTxBitrateBuffer,
    theIntervalBuffer);

  theElementID++;
  fprintf(pOutFile, "<area id=\"i%d\" alt=\"\" shape=\"rect\" coords=\"%d,%d,%d,%d\" title=\"%s\" onmouseover=\"return s('i%d')\" onmouseout=\"return c()\" />\n",
    theElementID, pX1, 0, pX2, kGraphHeight, theTitleText, theElementID);
}

// renderNumberBytes
// - renders pValue according to pFmtString into pBuffer
// - does magnitude shifting on pValue for 10^{0,3,6,9}
// - default format string is adaptive on 3 significant figures
// - returns pBuffer
char* renderNumberBytes(char* pBuffer, double pValue, const char* pFmtString) {
  double theScaling;
  char* theScalingUnit = 0;
  char theFmtString[kStaticStringSize];

  if (pValue < 1024.0) {
    theScaling = 1;
    theScalingUnit = "";
  } else if (pValue < (1024.0 * 1024.0)) {
    pValue /= 1024.0;
    theScalingUnit = "K";
  } else if (pValue < (1024.0 * 1024.0 * 1024.0)) {
    pValue /= 1024.0 * 1024.0;
    theScalingUnit = "M";
  } else if (pValue < (1024.0 * 1024.0 * 1024.0 * 1024.0)) {
    pValue /= 1024.0 * 1024.0 * 1024.0;
    theScalingUnit = "G";
  } else if (pValue < (1024.0 * 1024.0 * 1024.0 * 1024.0 * 1024.0)) {
    pValue /= 1024.0 * 1024.0 * 1024.0 * 1024.0;
    theScalingUnit = "T";
  }

  if (pFmtString) {
    strcpy(theFmtString, pFmtString);
  } else {
    if (pValue > 100) {
      strcpy(theFmtString, "%0.0f");
    } else if (pValue > 10) {
      strcpy(theFmtString, "%0.1f");
    } else {
      strcpy(theFmtString, "%0.2f");
    }
  }
  strcat(theFmtString, " %sB");


  sprintf(pBuffer, theFmtString, pValue, theScalingUnit);

  return pBuffer;
}

