//
// VIPS Resamplex (C Server).
// Portions from VIPS Thumbnailer, https://github.com/jcupitt/libvips/blob/master/tools/vipsthumbnail.c
//

#define _BSD_SOURCE // for `mkstemps`
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <vips/vips.h>

//
// § Definitions
//

//
// Linear vs. Perceptual. It would seem that linear vs. perceptual space is
// not a concern, since this program’s output will be solely used in a browser
// and not in further applications downstream.
//
#define LINEAR_PROCESSING 0

//
// Processing / Exporting Colour Space. Currently the only use case is for Web so everything
// gets exported in sRGB (if not already in that colour space).
//
#define PROCESSING_COLOUR_SPACE (LINEAR_PROCESSING ? VIPS_INTERPRETATION_scRGB : VIPS_INTERPRETATION_sRGB)
#define EXPORTING_COLOUR_SPACE VIPS_INTERPRETATION_sRGB

//
// Template for the temporary file. Presumablty if the program is to be ported elsewhere
// it will need to vary. Also it is plausible that the end user may want to customise this on their own as well
//
#define TEMPORARY_FILE_PATH_TEMPLATE "/tmp/converter-XXXXXX.jpg"

//
// Define whether memory can be used between passes. By default this means a 100MB high water mark
// within each pass and a maximum of 1MB of cached information can persist between passes.
// In the future this should be made tweakable, since code can be deployed in many places.
// Also, threading factors should be tweakable.
//
#define CACHE_BYTES_ALLOWED_WITHIN_EACH_PASS 104857600
#define CACHE_BYTES_ALLOWED_BETWEEN_EACH_PASS 0

//
// Define how many files can be held in the operation cache; by default this is 0
// as it is unlikely for a particular image to have things done to it repeatedly
//
#define CACHE_FILES_ALLOWED_WITHIN_EACH_PASS 0
#define CACHE_FILES_ALLOWED_BETWEEN_EACH_PASS 0

#define CACHE_OPS_ALLOWED_WITHIN_EACH_PASS 10
#define CACHE_OPS_ALLOWED_BETWEEN_EACH_PASS 0

//
// § Shrink Factor Calculation
//

double shrinkFactorForImage (VipsImage *fromImage, VipsAngle fromAngle, double toWidth, double toHeight) {
  if ((toWidth == 0.0) && (toHeight == 0.0)) {
    return 1.0;
  }
  const double fromWidth = (double)vips_image_get_width(fromImage);
  const double fromHeight = (double)vips_image_get_height(fromImage);
  switch (fromAngle) {
    case VIPS_ANGLE_D90:
    case VIPS_ANGLE_D270:
      return fmax((fromHeight / toWidth), (fromWidth / toHeight));
    default:
      return fmax((fromWidth / toWidth), (fromHeight / toHeight));
  }
}

unsigned int jpegShrinkFactorForFactor (double shrinkFactor) {
  if (LINEAR_PROCESSING) {
    return 1;
  } else {
    if (shrinkFactor >= 16) {
      return 8;
    } else if (shrinkFactor >= 8) {
      return 4;
    } else if (shrinkFactor >= 4) {
      return 2;
    } else {
      return 1;
    }
  }
}

void getImageShrinkFactorAndAngle (VipsImage *image, unsigned int toWidth, unsigned int toHeight, double *outShrinkFactor, VipsAngle *outAngle) {
  *outAngle = vips_autorot_get_angle(image);
  *outShrinkFactor = shrinkFactorForImage(image, *outAngle, (double)toWidth, (double)toHeight);
}

//
// § Image Loading
//

VipsImage * newImageFromGenericPath (char *filePath, unsigned int toWidth, unsigned int toHeight, double *outShrinkFactor, VipsAngle *outAngle) {
  VipsImage *loadedImage = vips_image_new_from_file(filePath, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
  getImageShrinkFactorAndAngle(loadedImage, toWidth, toHeight, outShrinkFactor, outAngle);
  return loadedImage;
}

VipsImage * newImageFromJPEGPath (char *filePath, unsigned int toWidth, unsigned int toHeight, double *outShrinkFactor, VipsAngle *outAngle) {
  VipsImage *headerImage = newImageFromGenericPath(filePath, toWidth, toHeight, outShrinkFactor, outAngle);
  g_object_unref(headerImage);

  VipsImage *loadedImage = vips_image_new_from_file(filePath, "access", VIPS_ACCESS_SEQUENTIAL, "shrink", jpegShrinkFactorForFactor(*outShrinkFactor), NULL);
  getImageShrinkFactorAndAngle(loadedImage, toWidth, toHeight, outShrinkFactor, outAngle);
  return loadedImage;
}

VipsImage * newImageFromPDFPath (char *filePath, unsigned int toWidth, unsigned int toHeight, double *outShrinkFactor, VipsAngle *outAngle) {
  VipsImage *headerImage = newImageFromGenericPath(filePath, toWidth, toHeight, outShrinkFactor, outAngle);
  g_object_unref(headerImage);

  VipsImage *loadedImage = vips_image_new_from_file(filePath, "access", VIPS_ACCESS_SEQUENTIAL, "scale", (1.0 / (*outShrinkFactor)), NULL);
  getImageShrinkFactorAndAngle(loadedImage, toWidth, toHeight, outShrinkFactor, outAngle);
  return loadedImage;
}

VipsImage * newImageFromSVGPath (char *filePath, unsigned int toWidth, unsigned int toHeight, double *outShrinkFactor, VipsAngle *outAngle) {
  //
  // Same underlying code path as PDF loading.
  //
  return newImageFromPDFPath(filePath, toWidth, toHeight, outShrinkFactor, outAngle);
}

VipsImage * newImageFromWebPPath (char *filePath, unsigned int toWidth, unsigned int toHeight, double *outShrinkFactor, VipsAngle *outAngle) {
  VipsImage *headerImage = newImageFromGenericPath(filePath, toWidth, toHeight, outShrinkFactor, outAngle);
  g_object_unref(headerImage);

  VipsImage *loadedImage = vips_image_new_from_file(filePath, "access", VIPS_ACCESS_SEQUENTIAL, "shrink", *outShrinkFactor, NULL);
  getImageShrinkFactorAndAngle(loadedImage, toWidth, toHeight, outShrinkFactor, outAngle);
  return loadedImage;
}

VipsImage * newImageFromPath (char *filePath, unsigned int toWidth, unsigned int toHeight, double *outShrinkFactor, VipsAngle *outAngle) {
  const char *loaderName = vips_foreign_find_load(filePath);

  if (!loaderName) {
    return NULL;
  }

  if (0 == strcmp(loaderName, "VipsForeignLoadJpegFile")) {
    return newImageFromJPEGPath(filePath, toWidth, toHeight, outShrinkFactor, outAngle);
  }

  if (0 == strcmp(loaderName, "VipsForeignLoadPdfFile")) {
    return newImageFromPDFPath(filePath, toWidth, toHeight, outShrinkFactor, outAngle);
  }

  if (0 == strcmp(loaderName, "VipsForeignLoadSvgFile")) {
    return newImageFromSVGPath(filePath, toWidth, toHeight, outShrinkFactor, outAngle);
  }

  if (0 == strcmp(loaderName, "VipsForeignLoadWebpFile")) {
    return newImageFromWebPPath(filePath, toWidth, toHeight, outShrinkFactor, outAngle);
  }

  return newImageFromGenericPath(filePath, toWidth, toHeight, outShrinkFactor, outAngle);
}

//
// § Image Shrinking
//

bool shouldImportColourProfileForImage (VipsImage *image) {
  const VipsInterpretation fromInterpretation = vips_image_get_interpretation(image);
  const VipsCoding fromCoding = vips_image_get_coding(image);
  const VipsBandFormat fromBandFormat = vips_image_get_format(image);

  if (LINEAR_PROCESSING || (fromInterpretation == VIPS_INTERPRETATION_CMYK)) {
    if (fromCoding == VIPS_CODING_NONE) {
      if ((fromBandFormat == VIPS_FORMAT_UCHAR) || (fromBandFormat == VIPS_FORMAT_USHORT)) {
        if (vips_image_get_typeof(image, VIPS_META_ICC_NAME)) {
          return true;
        }
      }
    }
  }

  return false;
}

bool shouldConvertImageToProcessingColourSpace (VipsImage *image) {
  const VipsInterpretation fromInterpretation = vips_image_get_interpretation(image);
  const VipsInterpretation toInterpretation = PROCESSING_COLOUR_SPACE;

  if (fromInterpretation != toInterpretation) {
    return true;
  } else {
    return false;
  }
}

bool shouldPremultiplyImage (VipsImage *image) {
  //
  // Check whether the image has an alpha channel; if it does then it needs to be pre-multiplied
  // before being shrunk, then un-premultiplied later after being shrunk.
  //
  // Note: VIPS Thumbnailer implements this in the same way by checking the number of bands
  // but really the function should be able to tell whether the image has an alpha channel
  // in order to return a proper answer. So we’re winging it at the moment.
  //
  // Note: libVIPS 8.5.0 (started 13 October 2016) has added vips_image_hasalpha()
  // so that may be quite useful.
  //
  const int fromNumberOfBands = vips_image_get_bands(image);

  if (fromNumberOfBands == 2) {
    return true;
  }

  if (fromNumberOfBands == 4) {
    const VipsInterpretation fromInterpretation = vips_image_get_interpretation(image);
    if (fromInterpretation != VIPS_INTERPRETATION_CMYK) {
      return true;
    }
  }

  if (fromNumberOfBands == 5) {
    return true;
  }

  return false;
}

bool shouldConvertImageToExportingColourSpace (VipsImage *image) {
  const VipsInterpretation fromInterpretation = vips_image_get_interpretation(image);
  const VipsInterpretation toInterpretation = EXPORTING_COLOUR_SPACE;

  if (fromInterpretation != toInterpretation) {
    return true;
  } else {
    return false;
  }
}

VipsImage *newThumbnailFromImage (VipsImage **localImages, VipsImage *parentImage, VipsAngle angle, double shrinkFactor) {
  VipsImage *currentImage = parentImage;
  bool havePremultiplied = false;
  VipsBandFormat imageBandFormatBeforePremultiplication = VIPS_FORMAT_NOTSET;

  //
  // 01 § Special unpacking for RAD
  //
  if (VIPS_CODING_RAD == vips_image_get_coding(currentImage)) {
    VipsImage *unpackedImage = NULL;
    if (vips_rad2float(currentImage, &unpackedImage, NULL)) {
      return NULL;
    }
    currentImage = localImages[0] = unpackedImage;
  }

  //
  // 02 § Import colour profile, if needed
  //
  if (shouldImportColourProfileForImage(currentImage)) {
    VipsImage *importedImage = NULL;
    if (vips_icc_import(currentImage, &importedImage, "embedded", TRUE, "pcs", VIPS_PCS_XYZ, NULL)) {
      return NULL;
    }
    currentImage = localImages[1] = importedImage;
  }

  //
  // 03 § Convert to device colour space, if needed
  //
  if (shouldConvertImageToProcessingColourSpace(currentImage)) {
    VipsImage *convertedImage = NULL;
    if (vips_colourspace(currentImage, &convertedImage, PROCESSING_COLOUR_SPACE, NULL)) {
      return NULL;
    }
    currentImage = localImages[2] = convertedImage;
  }

  //
  // 04 § Premultiply image, if needed
  //
  if (shouldPremultiplyImage(currentImage)) {
    imageBandFormatBeforePremultiplication = vips_image_get_format(currentImage);
    VipsImage *premultipliedImage = NULL;
    if (vips_premultiply(currentImage, &premultipliedImage, NULL)) {
      return NULL;
    }
    havePremultiplied = true;
    currentImage = localImages[3] = premultipliedImage;
  }

  //
  // 05 § Resize image
  //
  if (shrinkFactor != 1.0) {
    VipsImage *resizedImage = NULL;
    if (vips_resize(currentImage, &resizedImage, (1.0 / shrinkFactor), "centre", TRUE, NULL)) {
      return NULL;
    }
    currentImage = localImages[4] = resizedImage;
  }

  //
  // 06 § Unpremultiply image if premultiplied earlier
  // 07 § Cast image back to original band format if premultiplied earlier
  //
  if (havePremultiplied) {
    VipsImage *unpremultipliedImage = NULL;
    if (vips_unpremultiply(currentImage, &unpremultipliedImage, NULL)) {
      return NULL;
    }
    currentImage = localImages[5] = unpremultipliedImage;
    VipsImage *castImage = NULL;
    if (vips_cast(currentImage, &castImage, imageBandFormatBeforePremultiplication, NULL)) {
      return NULL;
    }
    currentImage = localImages[6] = unpremultipliedImage;
  }

  //
  // 08 § Transform image back to exporting colour space, if needed
  //
  if (shouldConvertImageToExportingColourSpace(currentImage)) {
    VipsImage *exportedImage = NULL;
    if (vips_colourspace(currentImage, &exportedImage, EXPORTING_COLOUR_SPACE, NULL)) {
      return NULL;
    }
    currentImage = localImages[7] = exportedImage;
  }

  //
  // 09 § Rotate image, if needed
  //
  if (angle != VIPS_ANGLE_D0) {
    VipsImage *copiedImage = localImages[8] = vips_image_new_memory();
    VipsImage *rotatedImage = NULL;
    if (vips_image_write(currentImage, copiedImage) || vips_rot(copiedImage, &rotatedImage, angle, NULL)) {
      return NULL;
    }
    currentImage = localImages[9] = rotatedImage;
    vips_autorot_remove_angle(currentImage);
  }

  //
  // 10 § Strip colour profile, if needed; let devices fall back to sRGB
  //
  if (vips_image_get_typeof(currentImage, VIPS_META_ICC_NAME)) {
    if (!vips_image_remove(currentImage, VIPS_META_ICC_NAME)) {
      return NULL;
    }
  }

  return currentImage;
}

//
// § Saving
//

char * pathWithWrittenDataForImage(VipsImage *image) {
  //
  // Find a temporary file and create it directly using mkstemps,
  // add a path extension so libVIPS knows which format to use.
  // Note: in the future we should allow configuring the server for PNG, JPEG and perhaps WebM.
  //
  static char * const fileTemplate = TEMPORARY_FILE_PATH_TEMPLATE;
  char *filePath = (char *)malloc(strlen(fileTemplate) + 1);
  strcpy(filePath, fileTemplate);
  int fileDescriptor = mkstemps(filePath, 4);
  if (!fileDescriptor) {
    fprintf(stderr, "ERROR - Unable to open exclusive temporary file\n");
    free(filePath);
    return NULL;
  }

  // Close file descriptor so as not to interfere with later writing
  close(fileDescriptor);

  //
  // Save the file. Try to catch any sort of error if needed.
  //
  if (0 != vips_image_write_to_file(image, filePath, "strip", TRUE, "interlace", TRUE, NULL)) {
    return NULL;
  }

  return filePath;
}

//
// § Glue
//

void processLine (char *lineBuffer) {
  //
  // Attempt to scan information from input string. Input must have the format of
  // <toWidth> <toHeight> <fromPath> and is supposed to be using a path that is within
  // a controlled temporary directory and a server-generated file name, instead of anywhere
  // in the filesystem and/or an user-provided file name.
  //
  static char fromFilePath[4096];
  unsigned int toWidth = 0;
  unsigned int toHeight = 0;
  if (3 != sscanf(lineBuffer, "%u %u %[^\n]s", &toWidth, &toHeight, fromFilePath)) {
    fprintf(stderr, "ERROR - line can not be parsed\n");
    return;
  }

  //
  // Try to open file; if shrink-on-load is supported load a smaller version immediately.
  // otherwise, load the full version later but still compute angle and shrink factor.
  //
  // If file can not be opened, fail.
  //
  VipsAngle fromAngle = VIPS_ANGLE_D0;
  double shrinkFactor = 1.0;
  VipsImage *fromImage = newImageFromPath(fromFilePath, toWidth, toHeight, &shrinkFactor, &fromAngle);
  if (!fromImage) {
    fprintf(stderr, "ERROR - Unable to open file\n");
    return;
  }

  //
  // The fun bit. Make thumbnail.
  //
  VipsObject *context = VIPS_OBJECT(vips_image_new());

  //
  // Stick the original image onto the context,
  // in case the incoming resample request is “0x0” which is simply a resave
  //
  vips_object_local(context, fromImage);

  VipsImage **localImages = (VipsImage **)vips_object_local_array(context, 10);
  VipsImage *thumbnailImage = newThumbnailFromImage(localImages, fromImage, fromAngle, shrinkFactor);

  if (!thumbnailImage) {
    fprintf(stderr, "ERROR - Unable to resize image\n");
    g_object_unref(context);
    return;
  }

  char *toFilePath = pathWithWrittenDataForImage(thumbnailImage);
  if (toFilePath) {
    printf("%s\n", toFilePath);

    // flush STDOUT immediately, so the application is not reliant on typical POSIX behaviour/
    // see http://stackoverflow.com/questions/1716296/why-does-printf-not-flush-after-the-call-unless-a-newline-is-in-the-format-strin
    fflush(stdout);

    free(toFilePath);
  } else {
    fprintf(stderr, "ERROR - Unable to write file\n");
  }
  g_object_unref(context);
}

int main (int argc, char **argv) {
  //
  // Suppress libVIPS warnings emitted via vips_warn(), by setting
  // IM_WARNING to a non-empty value. This avoids a case where when decoding an image,
  // warnings were emitted via STDOUT and they get to the calling process sooner than
  // a successful result (regardless) could be written to STDERR.
  //
  // Since this otherwise affects successful operation of the program
  // we will overwrite it if necessary.
  //
  if (setenv("IM_WARNING", "1", 1)) {
    return 1;
  }

  //
  // Suppress libVIPS warnings emitted via vips_vwarn(), by setting
  // VIPS_WARNING to a non-empty value. Same reason as above.
  //
  if (setenv("VIPS_WARNING", "1", 1)) {
    return 1;
  }

  //
  // Start libVIPS at the beginning of the program so once it starts accepting input
  // it is actually ready.
  //
  if (VIPS_INIT(argv[0])) {
    vips_error_exit(NULL);
  }

  //
  // Spin up an infinite loop to accept conversion requests from STDIN
  // with the format: <width> <height> <file path>.
  //
  // Note: fgets blocks, without burning CPU. Looks like the best function on macOS;
  // other ones burn CPU when idle.
  //
  static char lineBuffer[4096];

  while (fgets(lineBuffer, 4096, stdin)) {
    vips_cache_set_max_mem(CACHE_BYTES_ALLOWED_WITHIN_EACH_PASS);
    vips_cache_set_max_files(CACHE_FILES_ALLOWED_WITHIN_EACH_PASS);
    vips_cache_set_max(CACHE_OPS_ALLOWED_WITHIN_EACH_PASS);

    //
    // Start the main process loop. Spin up a VipsObject in order for
    // downstream functions to hang all dependent objects on this context object,
    // so it can all be de-allocated in one go later on.
    //
    processLine(lineBuffer);
    vips_error_clear();

    //
    // Empty cache but allow a higher high water mark within each pass,
    // if so desired. By default this is set to 0MB and 100MB respectively
    // but can be tweaked in the future.
    //
    vips_cache_set_max_mem(CACHE_BYTES_ALLOWED_BETWEEN_EACH_PASS);
    vips_cache_set_max_files(CACHE_FILES_ALLOWED_BETWEEN_EACH_PASS);
    vips_cache_set_max(CACHE_OPS_ALLOWED_BETWEEN_EACH_PASS);
  }

  vips_shutdown();
  return 0;
}