//////////////////////////////////////////////////////////////////////////////
// hseval.cpp
// 
//     This utility uses the Image and Compressor classes to compress a
// true color BMP file, decompress the image, and evaluate how closely
// the final image matches the source image.
//
//     Scoring an image is based on image size as well as image quality.
// The formula is:  score = 100 * (quality percentage - compressed percentage).
// Compressed percentage is:  Compressed image size / original BMP size
// Quality percentage is evauated functionally and is dependent on many
// different things.
///////////////////////////////////////////////////////////////////////////////

#include "Compressor.h"
#include <strings.h>
#include <fstream.h>
#include <strstream.h>
#include <math.h>

/* This structure is used in the analysis procedure to pass results
   through recursive function calls. */

struct Result
{
  double area;        // The number of pixels this result covers.
  double score;       // The score for the given area.
  double dupAvgH;     // Color averages for the given area.
  double dupAvgS;
  double dupAvgV;
  double masterAvgH;
  double masterAvgS;
  double masterAvgV;

  // Initialize every instance to 0 using a constructor.

  Result () : area(0), score(0), dupAvgH(0), dupAvgS(0), dupAvgV(0),
              masterAvgH(0), masterAvgS(0), masterAvgV(0) {};
};


/* This function adjusts the score in a result structure using a
   hacked evaluation of the HSV color averages. */

void adjustScore (Result & data)
{
  double hScore, sScore, vScore;
  double deltaH, deltaS, deltaV;
  double sum;

  // Calculate differences.

  deltaH = fabs (data.dupAvgH - data.masterAvgH);
  deltaS = fabs (data.dupAvgS - data.masterAvgS);
  deltaV = fabs (data.dupAvgV - data.masterAvgV);
  
  // Weigh each delta differently.  Note that scores can be negative.

  hScore = 1.0 - (deltaH * 10.0);
  sScore = 1.0 - (deltaS * 6.0);
  vScore = 1.0 - (deltaV * 12.0);

  // Combine with the actual score.

  sum = 3 + sqrt (sqrt (data.area)) - 1;

  data.score = ((data.score * (sqrt(sqrt(data.area)) - 1))
                + (hScore + sScore + vScore)) / sum;
}

/* This recursive function analyzes the image in the HSV color space by
   breaking the image into subsections and analyzing the subsections. */

void evaluate (Image & dup, Image & master,
               const int x, const int y, const int width, const int height,
               Result & total)
{
  Result nw, sw, ne, se;  // Compass directions, grand total.
  Pixel p;
  
  // If either the width or height < 1, do nothing.

  if (width < 1 || height < 1)
    return;
  
  // If the width and height is 1, then initialize the total
  //   to contain the evaluation of the single pixel and stop the recursion.

  else if (width == 1 && height == 1)
  {
    total.area = 1;
    p = dup.getPixel (x, y);
    total.dupAvgH = p.getHue ();
    total.dupAvgS = p.getSaturation ();
    total.dupAvgV = p.getIntensity ();
    p = master.getPixel (x, y);
    total.masterAvgH = p.getHue ();
    total.masterAvgS = p.getSaturation ();
    total.masterAvgV = p.getIntensity ();
    adjustScore (total);
    return;
  }

  // Otherwise, break the region into as many as four sub-regions, evaluate
  //   them, and combine the results.

  else
  {
    evaluate (dup, master, x, y, width/2, height/2, nw);
    evaluate (dup, master, x + width/2, y, width - width/2, height/2, ne);
    evaluate (dup, master, x, y + height/2, width/2, height - height/2, sw);
    evaluate (dup, master, x + width/2, y + height/2, width - width/2,
              height - height/2, ne);

    total.area = nw.area + ne.area + sw.area + se.area;
    total.score = (nw.score * nw.area + ne.score * ne.area +
                   sw.score * sw.area + se.score * se.area) / total.area;
    total.dupAvgH = (nw.dupAvgH * nw.area + ne.dupAvgH * ne.area +
                     sw.dupAvgH * sw.area + se.dupAvgH * se.area) / total.area;
    total.dupAvgS = (nw.dupAvgS * nw.area + ne.dupAvgS * ne.area +
                     sw.dupAvgS * sw.area + se.dupAvgS * se.area) / total.area;
    total.dupAvgV = (nw.dupAvgV * nw.area + ne.dupAvgV * ne.area +
                     sw.dupAvgV * sw.area + se.dupAvgV * se.area) / total.area;
    total.masterAvgH = (nw.masterAvgH * nw.area + ne.masterAvgH * ne.area +
                        sw.masterAvgH * sw.area + se.masterAvgH * se.area)
                       / total.area;
    total.masterAvgS = (nw.masterAvgS * nw.area + ne.masterAvgS * ne.area +
                        sw.masterAvgS * sw.area + se.masterAvgS * se.area)
                       / total.area;
    total.masterAvgV = (nw.masterAvgV * nw.area + ne.masterAvgV * ne.area +
                        sw.masterAvgV * sw.area + se.masterAvgV * se.area)
                       / total.area;

    adjustScore (total);
    return;
  }
}


int main (int argc, char ** argv)
{
  Image original, clone;
  double compressionScore;
  double qualityScore;
  int originalSize, compressedSize;
                           
  // Make sure we have the correct number and style of parameters.
  
  if ((argc != 2 && argc != 3) || argv[1] == NULL || argv[1][0] == 0)
  {
    cout << argv[0] << ':' << endl;
    cout << "  This program evaluates how well the compression class" << endl;
    cout << "compresses files.  It reads a single .BMP file." << endl;
    cout << "The .BMP file must be true color (24 bit), uncompressed," << endl;
    cout << "normal bitmap to be read.  A score is output. " << endl;
    cout << endl;
    cout << "Usage:" << endl;
    cout << "  " << argv[0] << " filename" << endl;
    cout << endl;
    cout << "\"filename\" must point to a valid .BMP file, see above." << endl;
    cout << endl;
    cout << "  You may also use this program to do a quality scoring" << endl;
    cout << "of two arbitrary images." << endl;
    cout << endl;
    cout << "Usage:" << endl;
    cout << "  " << argv[0] << " file1.bmp file2.bmp" << endl;
    cout << endl;
    cout << "\"file1\" and \"file2\" must point to valid .BMP files." << endl;
    cout << endl;
    return -1;
  }

  // Attempt to read in the image.

  if (! original.readBMP (argv[1], cout))
  {
    cout << "Evaluation failed." << endl;
    return -1;
  }

  // If there was a second image, read it.

  if (argc == 3)
  {
    if (! clone.readBMP (argv[2], cout))
    {
      cout << "Evaluation failed." << endl;
      return -1;
    }
  }

  // Otherwise, compress and decompress the first image.

  else
  {
    // Figure out the size of the original bitmap.

    originalSize = original.getHeight() * original.getWidth () * 3 + 54;

    if (originalSize < 1)
    {
      cout << "Zero-sized image." << endl;
      cout << "Evaluation failed." << endl;
      return -1;
    }

    // Create a string stream for storing the compressed image.

    char * streamData = new char[originalSize * 3];
    
    ostrstream compressedData(streamData, originalSize * 3);

    // Compress the image.

    Compressor highSchoolCompressor;

    highSchoolCompressor.compress (original, compressedData);

    // Extract the character stream size and create an input stream
    //   for decompression.

    compressedSize = compressedData.pcount ();

    istrstream inputData (streamData, compressedSize);

    // Decompress the file.

    clone = highSchoolCompressor.decompress (inputData);

    // Calculate the compression score.

    compressionScore = compressedSize / (double) originalSize;
  }
  
  // Calculate the quality score.

  Result total;

  evaluate (clone, original,
            0, 0, original.getWidth(), original.getHeight(), total); 
  
  qualityScore = total.score;

  // Calculate the total score.

  int totalScore = (int) ((qualityScore - compressionScore) * 100);

  // Report the scores.

  if (argc == 2)
  {
    cout << "Image: " << argv[1] << endl;
    cout << "Original size:   " << originalSize << endl;
    cout << "Compressed size: " << compressedSize << endl;
    cout << endl;
    cout << "Compression percentage: " << compressionScore*100 << "%" << endl;
  }
  
  cout << "Quality percentage:     " << qualityScore * 100 << "%" << endl;
  cout << endl;

  if (argc == 2)
  {
    cout << "Total score: " << totalScore << endl;
    cout << endl;
  }

  return 0;
}

