/* imagdiff:  compare two image files and writes differences to a third image
   file, using Imlib2 as an I/O and computational engine

   Copyright (C) 2018-2026 by Brian E. Lindholm.  This file is part of the
   littleutils utility set.

   The imagdiff utility 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, or (at your option) any later
   version.

   The imagdiff utility 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
   the littleutils.  If not, see <https://www.gnu.org/licenses/>. */

#include <config.h>

#define X_DISPLAY_MISSING
#ifdef HAVE_STDIO_H
# include <stdio.h>
#endif
#ifdef HAVE_STDLIB_H
# include <stdlib.h>
#endif

#ifdef HAVE_UNISTD_H
# include <unistd.h>
# define OPTEND -1
#else
# define OPTEND EOF
#endif
#ifdef HAVE_GETOPT_H
# include <getopt.h>
#endif

#ifdef __MINGW32__
extern int getopt(int argc, char * const *argv, const char *optstring);
extern char *optarg;
extern int optind;
#endif

#include <Imlib2.h>

#define MODE_GRAY 1
#define MODE_LIGHT_GRAY 2
#define MODE_DARK_GRAY 3
#define MODE_COLOR 4
#define MODE_STRETCH_COLOR 5

/* constants copied from pngrtran.c of libpng-1.6; many thanks to GRP et al */
#define R2G 6968
#define G2G 23434
#define B2G 2366
#define GSHIFT 15;

/* help function */
static void
help(FILE *where)
{
  fprintf(where, "imagdiff " PACKAGE_VERSION "\n"
    "usage: imagdiff [-f fuzz_dist] [-h(elp)] [-m mode] oldfile newfile deltafile\n"
    "       mode 1 = gray, 2 = light gray, 3 = dark gray, 4 = color,\n"
    "            5 = color-stretched\n");
}

/* main program */
int main(int argc, char *argv[])
{
  /* define main variables */
  Imlib_Image image_old, image_new, image_diff;
  Imlib_Load_Error rc;
  DATA32 *array_old, *array_new, *array_diff, data_old, data_new, data_diff, old_red, new_red, diff_red, old_green,
    new_green, diff_green, old_blue, new_blue, diff_blue, old_gray, new_gray, fallback;
  char *filename_old, *filename_new, *filename_diff;
  int image_width_old, image_width_new, image_width_diff, image_height_old, image_height_new, image_height_diff;
  int c, delta, fuzz, i, j, mode, max_delta;

  /* parse options */
  filename_old = NULL;
  filename_new = NULL;
  fuzz = 1;
  mode = MODE_GRAY;
  while ((c = getopt (argc, argv, "f:hm:")) != OPTEND) switch (c) {
    case 'f':
      fuzz = atoi(optarg);
      break;
    case 'h':
      help(stdout);
      return(0);
    case 'm':
      mode = atoi(optarg);
      break;
    case '?':
      help(stderr);
      return(1);
  }
  if ((fuzz < 0) || (fuzz > 255)) {
    help(stderr);
    return(1);
  }
  if ((mode < 1) || (mode > 5)) {
    help(stderr);
    return(1);
  }
  switch (mode) {
    case MODE_GRAY:
      fallback = 0x00FFFFFF;
      break;
    case MODE_DARK_GRAY:
      fallback = 0x00000000;
      break;
    case MODE_LIGHT_GRAY:
      fallback = 0x00FFFFFF;
      break;
    case MODE_COLOR:
      fallback = 0x007F7F7F;
      break;
    case MODE_STRETCH_COLOR:
      fallback = 0x007F7F7F;
      break;
    default:
      fprintf(stderr, "imagdiff error: this should never execute\n");
      return(3);
  }
  /* ensure that three filenames were given as input */
  if ((argc - optind) != 3) {
    fprintf(stderr, "imagdiff error: missing input filenames\n");
    help(stderr);
    return(1);
  }
  filename_old = argv[optind];
  filename_new = argv[optind + 1];
  filename_diff = argv[optind + 2];
  /* open the input files and determine image sizes */
  image_old = imlib_load_image_immediately_without_cache(filename_old);
  image_new = imlib_load_image_immediately_without_cache(filename_new);
  if ((image_old == NULL) && (image_new == NULL)) {
    fprintf(stderr, "imagdiff error: cannot process oldfile %s or newfile %s\n", filename_old, filename_new);
    return(2);
  }
  else if (image_old == NULL) {
    fprintf(stderr, "imagdiff warning: could not process oldfile %s; preparing fallback\n", filename_old);
    imlib_context_set_image(image_new);
    image_width_old = image_width_new = imlib_image_get_width();
    image_height_old = image_height_new = imlib_image_get_height();
    array_old = (DATA32 *) malloc((image_width_old * image_height_old) * sizeof(DATA32));
    for (j = 0; j < image_height_old; j++) for (i = 0; i < image_width_old; i++)
      array_old[j * image_width_old + i] = fallback;
    image_old = imlib_create_image_using_data(image_width_old, image_height_old, array_old);
    if (image_old == NULL) {
      fprintf(stderr, "imagdiff error: cannot prepare fallback old image\n");
      return(2);
    }
  }
  else if (image_new == NULL) {
    fprintf(stderr, "imagdiff warning: could not process newfile %s; preparing fallback\n", filename_new);
    imlib_context_set_image(image_old);
    image_width_new = image_width_old = imlib_image_get_width();
    image_height_new = image_height_old = imlib_image_get_height();
    array_new = (DATA32 *) malloc((image_width_new * image_height_new) * sizeof(DATA32));
    for (j = 0; j < image_height_new; j++) for (i = 0; i < image_width_new; i++)
      array_new[j * image_width_new + i] = fallback;
    image_new = imlib_create_image_using_data(image_width_new, image_height_new, array_new);
    if (image_new == NULL) {
      fprintf(stderr, "imagdiff error: cannot prepare fallback new image\n");
      return(2);
    }
  }
  else {
    imlib_context_set_image(image_old);
    image_width_old = imlib_image_get_width();
    image_height_old = imlib_image_get_height();
    imlib_context_set_image(image_new);
    image_width_new = imlib_image_get_width();
    image_height_new = imlib_image_get_height();
    if ((image_width_old != image_width_new) || (image_height_old != image_height_new))
      fprintf(stderr, "imagdiff warning: %s and %s differ in size; using overlap only\n", filename_old, filename_new);
  }
  /* prep for delta image */
  if (image_width_old < image_width_new)
    image_width_diff = image_width_old;
  else
    image_width_diff = image_width_new;
  if (image_height_old < image_height_new)
    image_height_diff = image_height_old;
  else
    image_height_diff = image_height_new;
  imlib_context_set_image(image_old);
  array_old = (DATA32 *) imlib_image_get_data();
  imlib_context_set_image(image_new);
  array_new = (DATA32 *) imlib_image_get_data();
  array_diff = (DATA32 *) malloc((image_width_diff * image_height_diff) * sizeof(DATA32));
  /* create delta image based on requested mode */
  switch (mode) {
    case MODE_GRAY:
      for (j = 0; j < image_height_diff; j++) for (i = 0; i < image_width_diff; i++) {
        data_old = array_old[j * image_width_old + i];
        old_red = (data_old >> 16) & 0xff;
        old_green = (data_old >> 8) & 0xff;
        old_blue = data_old & 0xff;
        old_gray = ((old_red * R2G) + (old_green * G2G) + (old_blue * B2G)) >> GSHIFT;
        data_new = array_new[j * image_width_new + i];
        new_red = (data_new >> 16) & 0xff;
        new_green = (data_new >> 8) & 0xff;
        new_blue = data_new & 0xff;
        new_gray = ((new_red * R2G) + (new_green * G2G) + (new_blue * B2G)) >> GSHIFT;
        delta = (int) new_gray - (int) old_gray;
        if (delta > fuzz) {
          diff_red = 255;
          diff_green = diff_blue = old_gray;
        }
        else if ((-delta) > fuzz) {
          diff_red = diff_green = new_gray;
          diff_blue = 255;
        }
        else
          diff_red = diff_green = diff_blue = (old_gray + new_gray) >> 1;
        data_diff = (diff_red << 16) + (diff_green << 8) + diff_blue;
        array_diff[j * image_width_diff + i] = data_diff;
      }
      break;
    case MODE_DARK_GRAY:
      for (j = 0; j < image_height_diff; j++) for (i = 0; i < image_width_diff; i++) {
        data_old = array_old[j * image_width_old + i];
        old_red = (data_old >> 16) & 0xff;
        old_green = (data_old >> 8) & 0xff;
        old_blue = data_old & 0xff;
        old_gray = ((old_red * R2G) + (old_green * G2G) + (old_blue * B2G)) >> GSHIFT;
        data_new = array_new[j * image_width_new + i];
        new_red = (data_new >> 16) & 0xff;
        new_green = (data_new >> 8) & 0xff;
        new_blue = data_new & 0xff;
        new_gray = ((new_red * R2G) + (new_green * G2G) + (new_blue * B2G)) >> GSHIFT;
        delta = (int) new_gray - (int) old_gray;
        if (delta > fuzz) {
          diff_red = 255;
          diff_green = diff_blue = old_gray;
        }
        else if ((-delta) > fuzz) {
          diff_red = diff_green = new_gray;
          diff_blue = 255;
        }
        else
          diff_red = diff_green = diff_blue = (old_gray + new_gray) >> 2;
        data_diff = (diff_red << 16) + (diff_green << 8) + diff_blue;
        array_diff[j * image_width_diff + i] = data_diff;
      }
      break;
    case MODE_LIGHT_GRAY:
      for (j = 0; j < image_height_diff; j++) for (i = 0; i < image_width_diff; i++) {
        data_old = array_old[j * image_width_old + i];
        old_red = (data_old >> 16) & 0xff;
        old_green = (data_old >> 8) & 0xff;
        old_blue = data_old & 0xff;
        old_gray = ((old_red * R2G) + (old_green * G2G) + (old_blue * B2G)) >> GSHIFT;
        data_new = array_new[j * image_width_new + i];
        new_red = (data_new >> 16) & 0xff;
        new_green = (data_new >> 8) & 0xff;
        new_blue = data_new & 0xff;
        new_gray = ((new_red * R2G) + (new_green * G2G) + (new_blue * B2G)) >> GSHIFT;
        delta = (int) new_gray - (int) old_gray;
        if (delta > fuzz) {
          diff_red = 255;
          diff_green = diff_blue = old_gray;
        }
        else if ((-delta) > fuzz) {
          diff_red = diff_green = new_gray;
          diff_blue = 255;
        }
        else
          diff_red = diff_green = diff_blue = (old_gray + new_gray + 512) >> 2;
        data_diff = (diff_red << 16) + (diff_green << 8) + diff_blue;
        array_diff[j * image_width_diff + i] = data_diff;
      }
      break;
    case MODE_COLOR:
      for (j = 0; j < image_height_diff; j++) for (i = 0; i < image_width_diff; i++) {
        data_old = array_old[j * image_width_old + i];
        old_red = (data_old >> 16) & 0xff;
        old_green = (data_old >> 8) & 0xff;
        old_blue = data_old & 0xff;
        data_new = array_new[j * image_width_new + i];
        new_red = (data_new >> 16) & 0xff;
        new_green = (data_new >> 8) & 0xff;
        new_blue = data_new & 0xff;
        diff_red = (new_red - old_red + 255) >> 1;
        diff_green = (new_green - old_green + 255) >> 1;
        diff_blue = (new_blue - old_blue + 255) >> 1;
        data_diff = (diff_red << 16) + (diff_green << 8) + diff_blue;
        array_diff[j * image_width_diff + i] = data_diff;
      }
      break;
    case MODE_STRETCH_COLOR:
      max_delta = 0;
      for (j = 0; j < image_height_diff; j++) for (i = 0; i < image_width_diff; i++) {
        data_old = array_old[j * image_width_old + i];
        old_red = (data_old >> 16) & 0xff;
        old_green = (data_old >> 8) & 0xff;
        old_blue = data_old & 0xff;
        data_new = array_new[j * image_width_new + i];
        new_red = (data_new >> 16) & 0xff;
        new_green = (data_new >> 8) & 0xff;
        new_blue = data_new & 0xff;
        if (new_red > old_red) {
          if ((new_red - old_red) > max_delta)
            max_delta = new_red - old_red;
        }
        else if ((old_red - new_red) > max_delta)
          max_delta = old_red - new_red;
        if (new_green > old_green) {
          if ((new_green - old_green) > max_delta)
            max_delta = new_green - old_green;
        }
        else if ((old_green - new_green) > max_delta)
          max_delta = old_green - new_green;
        if (new_blue > old_blue) {
          if ((new_blue - old_blue) > max_delta)
            max_delta = new_blue - old_blue;
        }
        else if ((old_blue - new_blue) > max_delta)
          max_delta = old_blue - new_blue;
      }
      for (j = 0; j < image_height_diff; j++) for (i = 0; i < image_width_diff; i++) {
        data_old = array_old[j * image_width_old + i];
        old_red = (data_old >> 16) & 0xff;
        old_green = (data_old >> 8) & 0xff;
        old_blue = data_old & 0xff;
        data_new = array_new[j * image_width_new + i];
        new_red = (data_new >> 16) & 0xff;
        new_green = (data_new >> 8) & 0xff;
        new_blue = data_new & 0xff;
        diff_red = (255 * (new_red - old_red) / max_delta + 255) >> 1;
        diff_green = (255 * (new_green - old_green) / max_delta + 255) >> 1;
        diff_blue = (255 * (new_blue - old_blue) / max_delta + 255) >> 1;
        data_diff = (diff_red << 16) + (diff_green << 8) + diff_blue;
        array_diff[j * image_width_diff + i] = data_diff;
      }
      break;
    default:
      fprintf(stderr, "imagdiff error: this should never execute\n");
      return(3);
  }
  /* write delta image to file */
  image_diff = imlib_create_image_using_data(image_width_diff, image_height_diff, array_diff);
  if (image_diff == NULL) {
    fprintf(stderr, "imagdiff error: can't prepare diff image %s\n", filename_diff);
    return(2);
  }
  imlib_context_set_image(image_diff);
  imlib_save_image_with_error_return(filename_diff, &rc);
  if (rc > 0) {
    fprintf(stderr, "imagdiff error: can't save diff image %s (rc = %d)\n", filename_diff, rc);
    return(2);
  }
  /* Clean up and quit */
  imlib_context_set_image(image_diff);
  imlib_free_image();
  imlib_context_set_image(image_new);
  imlib_free_image();
  imlib_context_set_image(image_old);
  imlib_free_image();
  return(0);
}
