// tga2jpg.cpp - jpge/jpgd example command line app.
// Public domain, Rich Geldreich <richgel99@gmail.com>
// Last updated May. 19, 2012

// Note: jpge.cpp/h and jpgd.cpp/h are completely standalone, i.e. they do not have any dependencies to each other.
#include "jpge.h"
#include "jpgd.h"
#include "stb_image.c"
#include "timer.h"
#include <ctype.h>

#if defined(_MSC_VER)
  #define strcasecmp _stricmp
#else
  #define strcpy_s(d, c, s) strcpy(d, s)
#endif

static int print_usage()
{
  printf("Usage: jpge [options] <source_file> <dest_file> <quality_factor>\n");
  printf("\nRequired parameters (must follow options):\n");
  printf("source_file: Source image file, in any format stb_image.c supports.\n");
  printf("dest_file: Destination JPEG file.\n");
  printf("quality_factor: 1-100, higher=better (only needed in compression mode)\n");
  printf("\nDefault mode compresses source_file to dest_file. Alternate modes:\n");
  printf("-x: Exhaustive compression test (only needs source_file)\n");
  printf("-d: Test jpgd.h. source_file must be JPEG, and dest_file must be .TGA\n");
  printf("\nOptions supported in all modes:\n");
  printf("-glogfilename.txt: Append output to log file\n");
  printf("\nOptions supported in compression mode (the default):\n");
  printf("-o: Enable optimized Huffman tables (slower, but smaller files)\n");
  printf("-luma: Output Y-only image\n");
  printf("-h1v1, -h2v1, -h2v2: Chroma subsampling (default is either Y-only or H2V2)\n");
  printf("-m: Test mem to mem compression (instead of mem to file)\n");
  printf("-wfilename.tga: Write decompressed image to filename.tga\n");
  printf("-s: Use stb_image.c to decompress JPEG image, instead of jpgd.cpp\n");
  printf("\nExample usages:\n");
  printf("Test compression: jpge orig.png comp.jpg 90\n");
  printf("Test decompression: jpge -d comp.jpg uncomp.tga\n");
  printf("Exhaustively test compressor: jpge -x orig.png\n");
  
  return EXIT_FAILURE;
}

static char s_log_filename[256];

static void log_printf(const char *pMsg, ...)
{
  va_list args;

  va_start(args, pMsg);
  char buf[2048];
  vsnprintf(buf, sizeof(buf) - 1, pMsg, args);
  buf[sizeof(buf) - 1] = '\0';
  va_end(args);

  printf("%s", buf);

  if (s_log_filename[0])
  {
    FILE *pFile = fopen(s_log_filename, "a+");
    if (pFile)
    {
      fprintf(pFile, "%s", buf);
      fclose(pFile);
    }
  }
}

static uint get_file_size(const char *pFilename)
{
  FILE *pFile = fopen(pFilename, "rb");
  if (!pFile) return 0;
  fseek(pFile, 0, SEEK_END);
  uint file_size = ftell(pFile);
  fclose(pFile);
  return file_size;
}

struct image_compare_results
{
  image_compare_results() { memset(this, 0, sizeof(*this)); }

  double max_err;
  double mean;
  double mean_squared;
  double root_mean_squared;
  double peak_snr;
};

static void get_pixel(int* pDst, const uint8 *pSrc, bool luma_only, int num_comps)
{
  int r, g, b;
  if (num_comps == 1)
  {
    r = g = b = pSrc[0];
  }
  else if (luma_only)
  {
    const int YR = 19595, YG = 38470, YB = 7471;
    r = g = b = (pSrc[0] * YR + pSrc[1] * YG + pSrc[2] * YB + 32768) / 65536;
  }
  else
  {
    r = pSrc[0]; g = pSrc[1]; b = pSrc[2];
  }
  pDst[0] = r; pDst[1] = g; pDst[2] = b;
}

// Compute image error metrics.
static void image_compare(image_compare_results &results, int width, int height, const uint8 *pComp_image, int comp_image_comps, const uint8 *pUncomp_image_data, int uncomp_comps, bool luma_only)
{
  double hist[256];
  memset(hist, 0, sizeof(hist));

  const uint first_channel = 0, num_channels = 3;
  for (int y = 0; y < height; y++)
  {
    for (int x = 0; x < width; x++)
    {
      int a[3]; get_pixel(a, pComp_image + (y * width + x) * comp_image_comps, luma_only, comp_image_comps);
      int b[3]; get_pixel(b, pUncomp_image_data + (y * width + x) * uncomp_comps, luma_only, uncomp_comps);
      for (uint c = 0; c < num_channels; c++)
        hist[labs(a[first_channel + c] - b[first_channel + c])]++;
    }
  }

  results.max_err = 0;
  double sum = 0.0f, sum2 = 0.0f;
  for (uint i = 0; i < 256; i++)
  {
    if (!hist[i])
      continue;
    if (i > results.max_err)
      results.max_err = i;
    double x = i * hist[i];
    sum += x;
    sum2 += i * x;
  }

  // See http://bmrc.berkeley.edu/courseware/cs294/fall97/assignment/psnr.html
  double total_values = width * height;

  results.mean = sum / total_values;
  results.mean_squared = sum2 / total_values;

  results.root_mean_squared = sqrt(results.mean_squared);

  if (!results.root_mean_squared)
    results.peak_snr = 1e+10f;
  else
    results.peak_snr = log10(255.0f / results.root_mean_squared) * 20.0f;
}

// Simple exhaustive test. Tries compressing/decompressing image using all supported quality, subsampling, and Huffman optimization settings.
static int exhausive_compression_test(const char *pSrc_filename, bool use_jpgd)
{
  int status = EXIT_SUCCESS;

  // Load the source image.
  const int req_comps = 3; // request RGB image
  int width = 0, height = 0, actual_comps = 0;
  uint8 *pImage_data = stbi_load(pSrc_filename, &width, &height, &actual_comps, req_comps);
  if (!pImage_data)
  {
    log_printf("Failed loading file \"%s\"!\n", pSrc_filename);
    return EXIT_FAILURE;
  }

  log_printf("Source file: \"%s\" Image resolution: %ix%i Actual comps: %i\n", pSrc_filename, width, height, actual_comps);

  int orig_buf_size = width * height * 3; // allocate a buffer that's hopefully big enough (this is way overkill for jpeg)
  if (orig_buf_size < 1024) orig_buf_size = 1024;
  void *pBuf = malloc(orig_buf_size);

  uint8 *pUncomp_image_data = NULL;

  double max_err = 0;
  double lowest_psnr = 9e+9;
  double threshold_psnr = 9e+9;
  double threshold_max_err = 0.0f;

  image_compare_results prev_results;

  for (uint quality_factor = 1; quality_factor <= 100; quality_factor++)
  {
    for (uint subsampling = 0; subsampling <= jpge::H2V2; subsampling++)
    {
      for (uint optimize_huffman_tables = 0; optimize_huffman_tables <= 1; optimize_huffman_tables++)
      {
        // Fill in the compression parameter structure.
        jpge::params params;
        params.m_quality = quality_factor;
        params.m_subsampling = static_cast<jpge::subsampling_t>(subsampling);
        params.m_two_pass_flag = (optimize_huffman_tables != 0);

        int comp_size = orig_buf_size;
        if (!jpge::compress_image_to_jpeg_file_in_memory(pBuf, comp_size, width, height, req_comps, pImage_data, params))
        {
          status = EXIT_FAILURE;
          goto failure;
        }

        int uncomp_width = 0, uncomp_height = 0, uncomp_actual_comps = 0, uncomp_req_comps = 3;
        free(pUncomp_image_data);
        if (use_jpgd)
          pUncomp_image_data = jpgd::decompress_jpeg_image_from_memory((const stbi_uc*)pBuf, comp_size, &uncomp_width, &uncomp_height, &uncomp_actual_comps, uncomp_req_comps);
        else
          pUncomp_image_data = stbi_load_from_memory((const stbi_uc*)pBuf, comp_size, &uncomp_width, &uncomp_height, &uncomp_actual_comps, uncomp_req_comps);
        if (!pUncomp_image_data)
        {
          status = EXIT_FAILURE;
          goto failure;
        }

        if ((uncomp_width != width) || (uncomp_height != height))
        {
          status = EXIT_FAILURE;
          goto failure;
        }

        image_compare_results results;
        image_compare(results, width, height, pImage_data, req_comps, pUncomp_image_data, uncomp_req_comps, (params.m_subsampling == jpge::Y_ONLY) || (actual_comps == 1) || (uncomp_actual_comps == 1));
        //log_printf("Q: %3u, S: %u, O: %u, CompSize: %7u, Error Max: %3.3f, Mean: %3.3f, Mean^2: %5.3f, RMSE: %3.3f, PSNR: %3.3f\n", quality_factor, subsampling, optimize_huffman_tables, comp_size, results.max_err, results.mean, results.mean_squared, results.root_mean_squared, results.peak_snr);
        log_printf("%3u, %u, %u, %7u, %3.3f, %3.3f, %5.3f, %3.3f, %3.3f\n", quality_factor, subsampling, optimize_huffman_tables, comp_size, results.max_err, results.mean, results.mean_squared, results.root_mean_squared, results.peak_snr);
        if (results.max_err > max_err) max_err = results.max_err;
        if (results.peak_snr < lowest_psnr) lowest_psnr = results.peak_snr;

        if (quality_factor == 1)
        {
          if (results.peak_snr < threshold_psnr)
            threshold_psnr = results.peak_snr;
          if (results.max_err > threshold_max_err)
            threshold_max_err = results.max_err;
        }
        else
        {
          // Couple empirically determined tests - worked OK on my test data set.
          if ((results.peak_snr < (threshold_psnr - 3.0f)) || (results.peak_snr < 6.0f)) 
          {
            status = EXIT_FAILURE;
            goto failure;
          }
          if (optimize_huffman_tables)
          {
            if ((prev_results.max_err != results.max_err) || (prev_results.peak_snr != results.peak_snr))
            {
              status = EXIT_FAILURE;
              goto failure;
            }
          }
        }

        prev_results = results;
      }
    }
  }

  log_printf("Max error: %f Lowest PSNR: %f\n", max_err, lowest_psnr);

failure:
  free(pImage_data);
  free(pBuf);
  free(pUncomp_image_data);

  log_printf((status == EXIT_SUCCESS) ? "Success.\n" : "Exhaustive test failed!\n");
  return status;
}

// Test JPEG file decompression using jpgd.h
static int test_jpgd(const char *pSrc_filename, const char *pDst_filename)
{
  // Load the source JPEG image.
  const int req_comps = 3; // request RGB image
  int width = 0, height = 0, actual_comps = 0;
  
  timer tm;
  tm.start();

  uint8 *pImage_data = jpgd::decompress_jpeg_image_from_file(pSrc_filename, &width, &height, &actual_comps, req_comps);

  tm.stop();

  if (!pImage_data)
  {
    log_printf("Failed loading JPEG file \"%s\"!\n", pSrc_filename);
    return EXIT_FAILURE;
  }
    
  log_printf("Source JPEG file: \"%s\", image resolution: %ix%i, actual comps: %i\n", pSrc_filename, width, height, actual_comps);
  
  log_printf("Decompression time: %3.3fms\n", tm.get_elapsed_ms());

  if (!stbi_write_tga(pDst_filename, width, height, req_comps, pImage_data))
  {
    log_printf("Failed writing image to file \"%s\"!\n", pDst_filename);
    free(pImage_data);
    return EXIT_FAILURE;
  }
  log_printf("Wrote decompressed image to TGA file \"%s\"\n", pDst_filename);
  
  log_printf("Success.\n");

  free(pImage_data);
  return EXIT_SUCCESS;
}

int main(int arg_c, char* ppArgs[])
{
  printf("jpge/jpgd example app\n");

  // Parse command line.
  bool run_exhausive_test = false;
  bool test_memory_compression = false;
  bool optimize_huffman_tables = false;
  int subsampling = -1;
  char output_filename[256] = "";
  bool use_jpgd = true;
  bool test_jpgd_decompression = false;

  int arg_index = 1;
  while ((arg_index < arg_c) && (ppArgs[arg_index][0] == '-'))
  {
    switch (tolower(ppArgs[arg_index][1]))
    {
    case 'd':
      test_jpgd_decompression = true;
      break;
    case 'g':
      strcpy_s(s_log_filename, sizeof(s_log_filename), &ppArgs[arg_index][2]);
      break;
    case 'x':
      run_exhausive_test = true;
      break;
    case 'm':
      test_memory_compression = true;
      break;
    case 'o':
      optimize_huffman_tables = true;
      break;
    case 'l':
      if (strcasecmp(&ppArgs[arg_index][1], "luma") == 0)
        subsampling = jpge::Y_ONLY;
      else
      {
        log_printf("Unrecognized option: %s\n", ppArgs[arg_index]);
        return EXIT_FAILURE;
      }
      break;
    case 'h':
      if (strcasecmp(&ppArgs[arg_index][1], "h1v1") == 0)
        subsampling = jpge::H1V1;
      else if (strcasecmp(&ppArgs[arg_index][1], "h2v1") == 0)
        subsampling = jpge::H2V1;
      else if (strcasecmp(&ppArgs[arg_index][1], "h2v2") == 0)
        subsampling = jpge::H2V2;
      else
      {
        log_printf("Unrecognized subsampling: %s\n", ppArgs[arg_index]);
        return EXIT_FAILURE;
      }
      break;
    case 'w':
    {
      strcpy_s(output_filename, sizeof(output_filename), &ppArgs[arg_index][2]);
      break;
    }
    case 's':
    {
      use_jpgd = false;
      break;
    }
    default:
      log_printf("Unrecognized option: %s\n", ppArgs[arg_index]);
      return EXIT_FAILURE;
    }
    arg_index++;
  }

  if (run_exhausive_test)
  {
    if ((arg_c - arg_index) < 1)
    {
      log_printf("Not enough parameters (expected source file)\n");
      return print_usage();
    }

    const char* pSrc_filename = ppArgs[arg_index++];
    return exhausive_compression_test(pSrc_filename, use_jpgd);
  }
  else if (test_jpgd_decompression)
  {
    if ((arg_c - arg_index) < 2)
    {
      log_printf("Not enough parameters (expected source and destination files)\n");
      return print_usage();
    }

    const char* pSrc_filename = ppArgs[arg_index++];
    const char* pDst_filename = ppArgs[arg_index++];
    return test_jpgd(pSrc_filename, pDst_filename);
  }

  // Test jpge
  if ((arg_c - arg_index) < 3)
  {
    log_printf("Not enough parameters (expected source file, dest file, quality factor to follow options)\n");
    return print_usage();
  }

  const char* pSrc_filename = ppArgs[arg_index++];
  const char* pDst_filename = ppArgs[arg_index++];

  int quality_factor = atoi(ppArgs[arg_index++]);
  if ((quality_factor < 1) || (quality_factor > 100))
  {
    log_printf("Quality factor must range from 1-100!\n");
    return EXIT_FAILURE;
  }

  // Load the source image.
  const int req_comps = 3; // request RGB image
  int width = 0, height = 0, actual_comps = 0;
  uint8 *pImage_data = stbi_load(pSrc_filename, &width, &height, &actual_comps, req_comps);
  if (!pImage_data)
  {
    log_printf("Failed loading file \"%s\"!\n", pSrc_filename);
    return EXIT_FAILURE;
  }

  log_printf("Source file: \"%s\", image resolution: %ix%i, actual comps: %i\n", pSrc_filename, width, height, actual_comps);

  // Fill in the compression parameter structure.
  jpge::params params;
  params.m_quality = quality_factor;
  params.m_subsampling = (subsampling < 0) ? ((actual_comps == 1) ? jpge::Y_ONLY : jpge::H2V2) : static_cast<jpge::subsampling_t>(subsampling);
  params.m_two_pass_flag = optimize_huffman_tables;

  log_printf("Writing JPEG image to file: %s\n", pDst_filename);

  timer tm;
  
  // Now create the JPEG file.
  if (test_memory_compression)
  {
    int buf_size = width * height * 3; // allocate a buffer that's hopefully big enough (this is way overkill for jpeg)
    if (buf_size < 1024) buf_size = 1024;
    void *pBuf = malloc(buf_size);

    tm.start();
    if (!jpge::compress_image_to_jpeg_file_in_memory(pBuf, buf_size, width, height, req_comps, pImage_data, params))
    {
       log_printf("Failed creating JPEG data!\n");
       return EXIT_FAILURE;
    }
    tm.stop();

    FILE *pFile = fopen(pDst_filename, "wb");
    if (!pFile)
    {
       log_printf("Failed creating file \"%s\"!\n", pDst_filename);
       return EXIT_FAILURE;
    }

    if (fwrite(pBuf, buf_size, 1, pFile) != 1)
    {
       log_printf("Failed writing to output file!\n");
       return EXIT_FAILURE;
    }

    if (fclose(pFile) == EOF)
    {
       log_printf("Failed writing to output file!\n");
       return EXIT_FAILURE;
    }
  }
  else
  {
    tm.start();

    if (!jpge::compress_image_to_jpeg_file(pDst_filename, width, height, req_comps, pImage_data, params))
    {
       log_printf("Failed writing to output file!\n");
       return EXIT_FAILURE;
    }
    tm.stop();
  }

  double total_comp_time = tm.get_elapsed_ms();

  const uint comp_file_size = get_file_size(pDst_filename);
  const uint total_pixels = width * height;
  log_printf("Compressed file size: %u, bits/pixel: %3.3f\n", comp_file_size, (comp_file_size * 8.0f) / total_pixels);

  // Now try loading the JPEG file using jpgd or stbi_image's JPEG decompressor.
  int uncomp_width = 0, uncomp_height = 0, uncomp_actual_comps = 0, uncomp_req_comps = 3;

  tm.start();
  uint8 *pUncomp_image_data;
  if (use_jpgd)
    pUncomp_image_data = jpgd::decompress_jpeg_image_from_file(pDst_filename, &uncomp_width, &uncomp_height, &uncomp_actual_comps, uncomp_req_comps);
  else
    pUncomp_image_data = stbi_load(pDst_filename, &uncomp_width, &uncomp_height, &uncomp_actual_comps, uncomp_req_comps);

  double total_uncomp_time = tm.get_elapsed_ms();

  if (!pUncomp_image_data)
  {
    log_printf("Failed loading compressed image file \"%s\"!\n", pDst_filename);
    return EXIT_FAILURE;
  }

  log_printf("Compression time: %3.3fms, Decompression time: %3.3fms\n", total_comp_time, total_uncomp_time);
  
  // Write uncompressed image.
  if (output_filename[0])
    stbi_write_tga(output_filename, uncomp_width, uncomp_height, uncomp_req_comps, pUncomp_image_data);

  if ((uncomp_width != width) || (uncomp_height != height))
  {
    log_printf("Loaded JPEG file has a different resolution than the original file!\n");
    return EXIT_FAILURE;
  }

  // Diff the original and compressed images.
  image_compare_results results;
  image_compare(results, width, height, pImage_data, req_comps, pUncomp_image_data, uncomp_req_comps, (params.m_subsampling == jpge::Y_ONLY) || (actual_comps == 1) || (uncomp_actual_comps == 1));
  log_printf("Error Max: %f, Mean: %f, Mean^2: %f, RMSE: %f, PSNR: %f\n", results.max_err, results.mean, results.mean_squared, results.root_mean_squared, results.peak_snr);

  log_printf("Success.\n");

  return EXIT_SUCCESS;
}
