/*!
  Copyright 2013 Lovell Fuller and others.
  SPDX-License-Identifier: Apache-2.0
*/

#include <algorithm>
#include <cstdlib>
#include <map>
#include <mutex>
#include <queue>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

#include <napi.h>
#include <vips/vips8>

#include "./common.h"

using vips::VImage;

namespace sharp {

  // Convenience methods to access the attributes of a Napi::Object
  bool HasAttr(Napi::Object obj, std::string attr) {
    return obj.Has(attr);
  }
  std::string AttrAsStr(Napi::Object obj, std::string attr) {
    return obj.Get(attr).As<Napi::String>();
  }
  std::string AttrAsStr(Napi::Object obj, unsigned int const attr) {
    return obj.Get(attr).As<Napi::String>();
  }
  uint32_t AttrAsUint32(Napi::Object obj, std::string attr) {
    return obj.Get(attr).As<Napi::Number>().Uint32Value();
  }
  int32_t AttrAsInt32(Napi::Object obj, std::string attr) {
    return obj.Get(attr).As<Napi::Number>().Int32Value();
  }
  int32_t AttrAsInt32(Napi::Object obj, unsigned int const attr) {
    return obj.Get(attr).As<Napi::Number>().Int32Value();
  }
  int64_t AttrAsInt64(Napi::Object obj, std::string attr) {
    return obj.Get(attr).As<Napi::Number>().Int64Value();
  }
  double AttrAsDouble(Napi::Object obj, std::string attr) {
    return obj.Get(attr).As<Napi::Number>().DoubleValue();
  }
  double AttrAsDouble(Napi::Object obj, unsigned int const attr) {
    return obj.Get(attr).As<Napi::Number>().DoubleValue();
  }
  bool AttrAsBool(Napi::Object obj, std::string attr) {
    return obj.Get(attr).As<Napi::Boolean>().Value();
  }
  std::vector<double> AttrAsVectorOfDouble(Napi::Object obj, std::string attr) {
    Napi::Array napiArray = obj.Get(attr).As<Napi::Array>();
    std::vector<double> vectorOfDouble(napiArray.Length());
    for (unsigned int i = 0; i < napiArray.Length(); i++) {
      vectorOfDouble[i] = AttrAsDouble(napiArray, i);
    }
    return vectorOfDouble;
  }
  std::vector<int32_t> AttrAsInt32Vector(Napi::Object obj, std::string attr) {
    Napi::Array array = obj.Get(attr).As<Napi::Array>();
    std::vector<int32_t> vector(array.Length());
    for (unsigned int i = 0; i < array.Length(); i++) {
      vector[i] = AttrAsInt32(array, i);
    }
    return vector;
  }

  // Create an InputDescriptor instance from a Napi::Object describing an input image
  InputDescriptor* CreateInputDescriptor(Napi::Object input) {
    InputDescriptor *descriptor = new InputDescriptor;
    if (HasAttr(input, "file")) {
      descriptor->file = AttrAsStr(input, "file");
    } else if (HasAttr(input, "buffer")) {
      Napi::Buffer<char> buffer = input.Get("buffer").As<Napi::Buffer<char>>();
      descriptor->bufferLength = buffer.Length();
      descriptor->buffer = buffer.Data();
      descriptor->isBuffer = true;
    }
    descriptor->failOn = AttrAsEnum<VipsFailOn>(input, "failOn", VIPS_TYPE_FAIL_ON);
    // Density for vector-based input
    if (HasAttr(input, "density")) {
      descriptor->density = AttrAsDouble(input, "density");
    }
    // Should we ignore any embedded ICC profile
    if (HasAttr(input, "ignoreIcc")) {
      descriptor->ignoreIcc = AttrAsBool(input, "ignoreIcc");
    }
    // Raw pixel input
    if (HasAttr(input, "rawChannels")) {
      descriptor->rawDepth = AttrAsEnum<VipsBandFormat>(input, "rawDepth", VIPS_TYPE_BAND_FORMAT);
      descriptor->rawChannels = AttrAsUint32(input, "rawChannels");
      descriptor->rawWidth = AttrAsUint32(input, "rawWidth");
      descriptor->rawHeight = AttrAsUint32(input, "rawHeight");
      descriptor->rawPremultiplied = AttrAsBool(input, "rawPremultiplied");
      descriptor->rawPageHeight = AttrAsUint32(input, "rawPageHeight");
    }
    // Multi-page input (GIF, TIFF, PDF)
    if (HasAttr(input, "pages")) {
      descriptor->pages = AttrAsInt32(input, "pages");
    }
    if (HasAttr(input, "page")) {
      descriptor->page = AttrAsUint32(input, "page");
    }
    // SVG
    if (HasAttr(input, "svgStylesheet")) {
      descriptor->svgStylesheet = AttrAsStr(input, "svgStylesheet");
    }
    if (HasAttr(input, "svgHighBitdepth")) {
      descriptor->svgHighBitdepth = AttrAsBool(input, "svgHighBitdepth");
    }
    // Multi-level input (OpenSlide)
    if (HasAttr(input, "openSlideLevel")) {
      descriptor->openSlideLevel = AttrAsUint32(input, "openSlideLevel");
    }
    // subIFD (OME-TIFF)
    if (HasAttr(input, "subifd")) {
      descriptor->tiffSubifd = AttrAsInt32(input, "tiffSubifd");
    }
    // // PDF background color
    if (HasAttr(input, "pdfBackground")) {
      descriptor->pdfBackground = AttrAsVectorOfDouble(input, "pdfBackground");
    }
    // Use JPEG 2000 oneshot mode?
    if (HasAttr(input, "jp2Oneshot")) {
      descriptor->jp2Oneshot = AttrAsBool(input, "jp2Oneshot");
    }
    // Create new image
    if (HasAttr(input, "createChannels")) {
      descriptor->createChannels = AttrAsUint32(input, "createChannels");
      descriptor->createWidth = AttrAsUint32(input, "createWidth");
      descriptor->createHeight = AttrAsUint32(input, "createHeight");
      descriptor->createPageHeight = AttrAsUint32(input, "createPageHeight");
      if (HasAttr(input, "createNoiseType")) {
        descriptor->createNoiseType = AttrAsStr(input, "createNoiseType");
        descriptor->createNoiseMean = AttrAsDouble(input, "createNoiseMean");
        descriptor->createNoiseSigma = AttrAsDouble(input, "createNoiseSigma");
      } else {
        descriptor->createBackground = AttrAsVectorOfDouble(input, "createBackground");
      }
    }
    // Create new image with text
    if (HasAttr(input, "textValue")) {
      descriptor->textValue = AttrAsStr(input, "textValue");
      if (HasAttr(input, "textFont")) {
        descriptor->textFont = AttrAsStr(input, "textFont");
      }
      if (HasAttr(input, "textFontfile")) {
        descriptor->textFontfile = AttrAsStr(input, "textFontfile");
      }
      if (HasAttr(input, "textWidth")) {
        descriptor->textWidth = AttrAsUint32(input, "textWidth");
      }
      if (HasAttr(input, "textHeight")) {
        descriptor->textHeight = AttrAsUint32(input, "textHeight");
      }
      if (HasAttr(input, "textAlign")) {
        descriptor->textAlign = AttrAsEnum<VipsAlign>(input, "textAlign", VIPS_TYPE_ALIGN);
      }
      if (HasAttr(input, "textJustify")) {
        descriptor->textJustify = AttrAsBool(input, "textJustify");
      }
      if (HasAttr(input, "textDpi")) {
        descriptor->textDpi = AttrAsUint32(input, "textDpi");
      }
      if (HasAttr(input, "textRgba")) {
        descriptor->textRgba = AttrAsBool(input, "textRgba");
      }
      if (HasAttr(input, "textSpacing")) {
        descriptor->textSpacing = AttrAsUint32(input, "textSpacing");
      }
      if (HasAttr(input, "textWrap")) {
        descriptor->textWrap = AttrAsEnum<VipsTextWrap>(input, "textWrap", VIPS_TYPE_TEXT_WRAP);
      }
    }
    // Join images together
    if (HasAttr(input, "joinAnimated")) {
      descriptor->joinAnimated = AttrAsBool(input, "joinAnimated");
    }
    if (HasAttr(input, "joinAcross")) {
      descriptor->joinAcross = AttrAsUint32(input, "joinAcross");
    }
    if (HasAttr(input, "joinShim")) {
      descriptor->joinShim = AttrAsUint32(input, "joinShim");
    }
    if (HasAttr(input, "joinBackground")) {
      descriptor->joinBackground = AttrAsVectorOfDouble(input, "joinBackground");
    }
    if (HasAttr(input, "joinHalign")) {
      descriptor->joinHalign = AttrAsEnum<VipsAlign>(input, "joinHalign", VIPS_TYPE_ALIGN);
    }
    if (HasAttr(input, "joinValign")) {
      descriptor->joinValign = AttrAsEnum<VipsAlign>(input, "joinValign", VIPS_TYPE_ALIGN);
    }
    descriptor->limitInputPixels = static_cast<uint64_t>(AttrAsInt64(input, "limitInputPixels"));
    descriptor->limitInputChannels = static_cast<uint64_t>(AttrAsInt64(input, "limitInputChannels"));
    if (HasAttr(input, "access")) {
      descriptor->access = AttrAsBool(input, "sequentialRead") ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
    }
    // Remove safety features and allow unlimited input
    descriptor->unlimited = AttrAsBool(input, "unlimited");
    // Use the EXIF orientation to auto orient the image
    descriptor->autoOrient = AttrAsBool(input, "autoOrient");
    return descriptor;
  }

  // How many tasks are in the queue?
  std::atomic<int> counterQueue{0};

  // How many tasks are being processed?
  std::atomic<int> counterProcess{0};

  // Filename extension checkers
  static bool EndsWith(std::string const &str, std::string const &end) {
    return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end);
  }
  bool IsJpeg(std::string const &str) {
    return EndsWith(str, ".jpg") || EndsWith(str, ".jpeg") || EndsWith(str, ".JPG") || EndsWith(str, ".JPEG");
  }
  bool IsPng(std::string const &str) {
    return EndsWith(str, ".png") || EndsWith(str, ".PNG");
  }
  bool IsWebp(std::string const &str) {
    return EndsWith(str, ".webp") || EndsWith(str, ".WEBP");
  }
  bool IsGif(std::string const &str) {
    return EndsWith(str, ".gif") || EndsWith(str, ".GIF");
  }
  bool IsJp2(std::string const &str) {
    return EndsWith(str, ".jp2") || EndsWith(str, ".jpx") || EndsWith(str, ".j2k") || EndsWith(str, ".j2c")
      || EndsWith(str, ".JP2") || EndsWith(str, ".JPX") || EndsWith(str, ".J2K") || EndsWith(str, ".J2C");
  }
  bool IsTiff(std::string const &str) {
    return EndsWith(str, ".tif") || EndsWith(str, ".tiff") || EndsWith(str, ".TIF") || EndsWith(str, ".TIFF");
  }
  bool IsHeic(std::string const &str) {
    return EndsWith(str, ".heic") || EndsWith(str, ".HEIC");
  }
  bool IsHeif(std::string const &str) {
    return EndsWith(str, ".heif") || EndsWith(str, ".HEIF") || IsHeic(str) || IsAvif(str);
  }
  bool IsAvif(std::string const &str) {
    return EndsWith(str, ".avif") || EndsWith(str, ".AVIF");
  }
  bool IsJxl(std::string const &str) {
    return EndsWith(str, ".jxl") || EndsWith(str, ".JXL");
  }
  bool IsDz(std::string const &str) {
    return EndsWith(str, ".dzi") || EndsWith(str, ".DZI");
  }
  bool IsDzZip(std::string const &str) {
    return EndsWith(str, ".zip") || EndsWith(str, ".ZIP") || EndsWith(str, ".szi") || EndsWith(str, ".SZI");
  }
  bool IsV(std::string const &str) {
    return EndsWith(str, ".v") || EndsWith(str, ".V") || EndsWith(str, ".vips") || EndsWith(str, ".VIPS");
  }

  /*
    Trim space from end of string.
  */
  std::string TrimEnd(std::string const &str) {
    return str.substr(0, str.find_last_not_of(" \n\r\f") + 1);
  }

  /*
    Provide a string identifier for the given image type.
  */
  std::string ImageTypeId(ImageType const imageType) {
    std::string id;
    switch (imageType) {
      case ImageType::JPEG: id = "jpeg"; break;
      case ImageType::PNG: id = "png"; break;
      case ImageType::WEBP: id = "webp"; break;
      case ImageType::TIFF: id = "tiff"; break;
      case ImageType::GIF: id = "gif"; break;
      case ImageType::JP2: id = "jp2"; break;
      case ImageType::SVG: id = "svg"; break;
      case ImageType::HEIF: id = "heif"; break;
      case ImageType::PDF: id = "pdf"; break;
      case ImageType::MAGICK: id = "magick"; break;
      case ImageType::OPENSLIDE: id = "openslide"; break;
      case ImageType::PPM: id = "ppm"; break;
      case ImageType::FITS: id = "fits"; break;
      case ImageType::EXR: id = "exr"; break;
      case ImageType::JXL: id = "jxl"; break;
      case ImageType::RAD: id = "rad"; break;
      case ImageType::DCRAW: id = "dcraw"; break;
      case ImageType::UHDR: id = "uhdr"; break;
      case ImageType::VIPS: id = "vips"; break;
      case ImageType::RAW: id = "raw"; break;
      case ImageType::UNKNOWN: id = "unknown"; break;
      case ImageType::MISSING: id = "missing"; break;
    }
    return id;
  }

  /**
   * Regenerate this table with something like:
   *
   * $ vips -l foreign | grep -i load | awk '{ print $2, $1; }'
   *
   * Plus a bit of editing.
   */
  std::map<std::string, ImageType> loaderToType = {
    { "VipsForeignLoadJpegFile", ImageType::JPEG },
    { "VipsForeignLoadJpegBuffer", ImageType::JPEG },
    { "VipsForeignLoadPngFile", ImageType::PNG },
    { "VipsForeignLoadPngBuffer", ImageType::PNG },
    { "VipsForeignLoadWebpFile", ImageType::WEBP },
    { "VipsForeignLoadWebpBuffer", ImageType::WEBP },
    { "VipsForeignLoadTiffFile", ImageType::TIFF },
    { "VipsForeignLoadTiffBuffer", ImageType::TIFF },
    { "VipsForeignLoadGifFile", ImageType::GIF },
    { "VipsForeignLoadGifBuffer", ImageType::GIF },
    { "VipsForeignLoadNsgifFile", ImageType::GIF },
    { "VipsForeignLoadNsgifBuffer", ImageType::GIF },
    { "VipsForeignLoadJp2kBuffer", ImageType::JP2 },
    { "VipsForeignLoadJp2kFile", ImageType::JP2 },
    { "VipsForeignLoadSvgFile", ImageType::SVG },
    { "VipsForeignLoadSvgBuffer", ImageType::SVG },
    { "VipsForeignLoadHeifFile", ImageType::HEIF },
    { "VipsForeignLoadHeifBuffer", ImageType::HEIF },
    { "VipsForeignLoadPdfFile", ImageType::PDF },
    { "VipsForeignLoadPdfBuffer", ImageType::PDF },
    { "VipsForeignLoadMagickFile", ImageType::MAGICK },
    { "VipsForeignLoadMagickBuffer", ImageType::MAGICK },
    { "VipsForeignLoadMagick7File", ImageType::MAGICK },
    { "VipsForeignLoadMagick7Buffer", ImageType::MAGICK },
    { "VipsForeignLoadOpenslideFile", ImageType::OPENSLIDE },
    { "VipsForeignLoadPpmFile", ImageType::PPM },
    { "VipsForeignLoadFitsFile", ImageType::FITS },
    { "VipsForeignLoadOpenexr", ImageType::EXR },
    { "VipsForeignLoadJxlFile", ImageType::JXL },
    { "VipsForeignLoadJxlBuffer", ImageType::JXL },
    { "VipsForeignLoadRadFile", ImageType::RAD },
    { "VipsForeignLoadRadBuffer", ImageType::RAD },
    { "VipsForeignLoadDcRawFile", ImageType::DCRAW },
    { "VipsForeignLoadDcRawBuffer", ImageType::DCRAW },
    { "VipsForeignLoadUhdr", ImageType::UHDR },
    { "VipsForeignLoadUhdrFile", ImageType::UHDR },
    { "VipsForeignLoadUhdrBuffer", ImageType::UHDR },
    { "VipsForeignLoadVips", ImageType::VIPS },
    { "VipsForeignLoadVipsFile", ImageType::VIPS },
    { "VipsForeignLoadRaw", ImageType::RAW }
  };

  /*
    Determine image format of a buffer.
  */
  ImageType DetermineImageType(void *buffer, size_t const length) {
    ImageType imageType = ImageType::UNKNOWN;
    char const *load = vips_foreign_find_load_buffer(buffer, length);
    if (load != nullptr) {
      auto it = loaderToType.find(load);
      if (it != loaderToType.end()) {
        imageType = it->second;
      }
    }
    if (imageType == ImageType::UHDR) {
      imageType = ImageType::JPEG;
    }
    return imageType;
  }

  /*
    Determine image format, reads the first few bytes of the file
  */
  ImageType DetermineImageType(char const *file) {
    ImageType imageType = ImageType::UNKNOWN;
    char const *load = vips_foreign_find_load(file);
    if (load != nullptr) {
      auto it = loaderToType.find(load);
      if (it != loaderToType.end()) {
        imageType = it->second;
      }
    } else {
      if (EndsWith(vips::VError().what(), " does not exist\n")) {
        imageType = ImageType::MISSING;
      }
    }
    if (imageType == ImageType::UHDR) {
      imageType = ImageType::JPEG;
    }
    return imageType;
  }

  /*
    Does this image type support multiple pages?
  */
  bool ImageTypeSupportsPage(ImageType imageType) {
    return
      imageType == ImageType::WEBP ||
      imageType == ImageType::MAGICK ||
      imageType == ImageType::GIF ||
      imageType == ImageType::JP2 ||
      imageType == ImageType::TIFF ||
      imageType == ImageType::HEIF ||
      imageType == ImageType::PDF;
  }

  /*
    Does this image type support removal of safety limits?
  */
  bool ImageTypeSupportsUnlimited(ImageType imageType) {
    return
      imageType == ImageType::JPEG ||
      imageType == ImageType::PNG ||
      imageType == ImageType::SVG ||
      imageType == ImageType::TIFF ||
      imageType == ImageType::HEIF;
  }

  /*
    Format-specific options builder
  */
  vips::VOption* GetOptionsForImageType(ImageType imageType, InputDescriptor *descriptor) {
    vips::VOption *option = VImage::option()
      ->set("access", descriptor->access)
      ->set("fail_on", descriptor->failOn);
    if (descriptor->unlimited && ImageTypeSupportsUnlimited(imageType)) {
      option->set("unlimited", true);
    }
    if (ImageTypeSupportsPage(imageType)) {
      option->set("n", descriptor->pages);
      option->set("page", std::max(0, descriptor->page));
    }
    switch (imageType) {
      case ImageType::SVG:
        option->set("dpi", descriptor->density)
              ->set("stylesheet", descriptor->svgStylesheet.data())
              ->set("high_bitdepth", descriptor->svgHighBitdepth);
        break;
      case ImageType::TIFF:
        option->set("subifd", descriptor->tiffSubifd);
        break;
      case ImageType::PDF:
        option->set("dpi", descriptor->density)
              ->set("background", descriptor->pdfBackground);
        break;
      case ImageType::OPENSLIDE:
        option->set("level", descriptor->openSlideLevel);
        break;
      case ImageType::JP2:
        option->set("oneshot", descriptor->jp2Oneshot);
        break;
      case ImageType::MAGICK:
        option->set("density", std::to_string(descriptor->density).data());
        break;
      default:
        break;
    }
    return option;
  }

  /*
    Should HEIF image be re-opened using the primary item?
  */
  static bool HeifPrimaryPageReopen(VImage image, InputDescriptor *descriptor) {
    if (image.get_typeof(VIPS_META_N_PAGES) == G_TYPE_INT && image.get_typeof("heif-primary") == G_TYPE_INT) {
      if (image.get_int(VIPS_META_N_PAGES) > 1 && descriptor->pages == 1 && descriptor->page == -1) {
        int const pagePrimary = image.get_int("heif-primary");
        if (pagePrimary != 0) {
          descriptor->page = pagePrimary;
          return true;
        }
      }
    }
    return false;
  }

  /*
    Open an image from the given InputDescriptor (filesystem, compressed buffer, raw pixel data)
  */
  std::tuple<VImage, ImageType> OpenInput(InputDescriptor *descriptor) {
    VImage image;
    ImageType imageType;
    if (descriptor->isBuffer) {
      if (descriptor->rawChannels > 0) {
        // Raw, uncompressed pixel data
        bool const is8bit = vips_band_format_is8bit(descriptor->rawDepth);
        image = VImage::new_from_memory(descriptor->buffer, descriptor->bufferLength,
          descriptor->rawWidth, descriptor->rawHeight, descriptor->rawChannels, descriptor->rawDepth);
        if (descriptor->rawChannels < 3) {
          image.get_image()->Type = is8bit ? VIPS_INTERPRETATION_B_W : VIPS_INTERPRETATION_GREY16;
        } else {
          image.get_image()->Type = is8bit ? VIPS_INTERPRETATION_sRGB : VIPS_INTERPRETATION_RGB16;
        }
        if (descriptor->rawPageHeight > 0) {
          image.set(VIPS_META_PAGE_HEIGHT, descriptor->rawPageHeight);
          image.set(VIPS_META_N_PAGES, static_cast<int>(descriptor->rawHeight / descriptor->rawPageHeight));
        }
        if (descriptor->rawPremultiplied) {
          image = image.unpremultiply();
        }
        imageType = ImageType::RAW;
      } else {
        // Compressed data
        imageType = DetermineImageType(descriptor->buffer, descriptor->bufferLength);
        if (imageType != ImageType::UNKNOWN) {
          try {
            vips::VOption *option = GetOptionsForImageType(imageType, descriptor);
            image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option);
            if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
              image = SetDensity(image, descriptor->density);
            } else if (imageType == ImageType::HEIF && HeifPrimaryPageReopen(image, descriptor)) {
              option = GetOptionsForImageType(imageType, descriptor);
              image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option);
            }
          } catch (std::runtime_error const &err) {
            throw std::runtime_error(std::string("Input buffer has corrupt header: ") + err.what());
          }
        } else {
          throw std::runtime_error("Input buffer contains unsupported image format");
        }
      }
    } else {
      int const channels = descriptor->createChannels;
      if (channels > 0) {
        // Create new image
        if (descriptor->createNoiseType == "gaussian") {
          std::vector<VImage> bands = {};
          bands.reserve(channels);
          for (int _band = 0; _band < channels; _band++) {
            bands.push_back(VImage::gaussnoise(descriptor->createWidth, descriptor->createHeight, VImage::option()
              ->set("mean", descriptor->createNoiseMean)
              ->set("sigma", descriptor->createNoiseSigma)));
          }
          image = VImage::bandjoin(bands).copy(VImage::option()->set("interpretation",
            channels < 3 ? VIPS_INTERPRETATION_B_W: VIPS_INTERPRETATION_sRGB));
        } else {
          std::vector<double> background = {
            descriptor->createBackground[0],
            descriptor->createBackground[1],
            descriptor->createBackground[2]
          };
          if (channels == 4) {
            background.push_back(descriptor->createBackground[3]);
          }
          image = VImage::new_matrix(descriptor->createWidth, descriptor->createHeight)
            .copy(VImage::option()->set("interpretation",
              channels < 3 ? VIPS_INTERPRETATION_B_W : VIPS_INTERPRETATION_sRGB))
            .new_from_image(background);
        }
        if (descriptor->createPageHeight > 0) {
          image.set(VIPS_META_PAGE_HEIGHT, descriptor->createPageHeight);
          image.set(VIPS_META_N_PAGES, static_cast<int>(descriptor->createHeight / descriptor->createPageHeight));
        }
        image = image.cast(VIPS_FORMAT_UCHAR);
        imageType = ImageType::RAW;
      } else if (descriptor->textValue.length() > 0) {
        // Create a new image with text
        vips::VOption *textOptions = VImage::option()
          ->set("align", descriptor->textAlign)
          ->set("justify", descriptor->textJustify)
          ->set("rgba", descriptor->textRgba)
          ->set("spacing", descriptor->textSpacing)
          ->set("wrap", descriptor->textWrap)
          ->set("autofit_dpi", &descriptor->textAutofitDpi);
        if (descriptor->textWidth > 0) {
          textOptions->set("width", descriptor->textWidth);
        }
        // Ignore dpi if height is set
        if (descriptor->textWidth > 0 && descriptor->textHeight > 0) {
          textOptions->set("height", descriptor->textHeight);
        } else if (descriptor->textDpi > 0) {
          textOptions->set("dpi", descriptor->textDpi);
        }
        if (descriptor->textFont.length() > 0) {
          textOptions->set("font", const_cast<char*>(descriptor->textFont.data()));
        }
        if (descriptor->textFontfile.length() > 0) {
          textOptions->set("fontfile", const_cast<char*>(descriptor->textFontfile.data()));
        }
        image = VImage::text(const_cast<char *>(descriptor->textValue.data()), textOptions);
        if (!descriptor->textRgba) {
          image = image.copy(VImage::option()->set("interpretation", VIPS_INTERPRETATION_B_W));
        }
        imageType = ImageType::RAW;
      } else {
        // From filesystem
        imageType = DetermineImageType(descriptor->file.data());
        if (imageType == ImageType::MISSING) {
          if (descriptor->file.find("<svg") != std::string::npos) {
            throw std::runtime_error("Input file is missing, did you mean "
              "sharp(Buffer.from('" + descriptor->file.substr(0, 8) + "...')?");
          }
          throw std::runtime_error("Input file is missing: " + descriptor->file);
        }
        if (imageType != ImageType::UNKNOWN) {
          try {
            vips::VOption *option = GetOptionsForImageType(imageType, descriptor);
            image = VImage::new_from_file(descriptor->file.data(), option);
            if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
              image = SetDensity(image, descriptor->density);
            } else if (imageType == ImageType::HEIF && HeifPrimaryPageReopen(image, descriptor)) {
              option = GetOptionsForImageType(imageType, descriptor);
              image = VImage::new_from_file(descriptor->file.data(), option);
            }
          } catch (std::runtime_error const &err) {
            throw std::runtime_error(std::string("Input file has corrupt header: ") + err.what());
          }
        } else {
          throw std::runtime_error("Input file contains unsupported image format");
        }
      }
    }

    // Limit input images to a given number of pixels, where pixels = width * height
    if (descriptor->limitInputPixels > 0 &&
      static_cast<uint64_t>(image.width()) * image.height() > descriptor->limitInputPixels) {
      throw std::runtime_error("Input image exceeds pixel limit");
    }
    if (descriptor->limitInputChannels > 0 && static_cast<uint64_t>(image.bands()) > descriptor->limitInputChannels) {
      throw std::runtime_error("Input image exceeds channel limit");
    }
    return std::make_tuple(image, imageType);
  }

  /*
    Does this image have an embedded profile?
  */
  bool HasProfile(VImage image) {
    return image.get_typeof(VIPS_META_ICC_NAME) == VIPS_TYPE_BLOB;
  }

  /*
    Get copy of embedded profile.
  */
  std::pair<char*, size_t> GetProfile(VImage image) {
    std::pair<char*, size_t> icc(nullptr, 0);
    if (HasProfile(image)) {
      size_t length;
      const void *data = image.get_blob(VIPS_META_ICC_NAME, &length);
      icc.first = static_cast<char*>(vips_malloc(reinterpret_cast<VipsObject*>(image.get_image()), length));
      icc.second = length;
      memcpy(icc.first, data, length);
    }
    return icc;
  }

  /*
    Set embedded profile.
  */
  VImage SetProfile(VImage image, std::pair<char*, size_t> icc) {
    if (icc.first != nullptr) {
      image = image.copy();
      image.set(VIPS_META_ICC_NAME, nullptr, icc.first, icc.second);
    }
    return image;
  }

  static void* RemoveExifCallback(VipsImage *image, char const *field, GValue *value, void *data) {
    std::vector<std::string> *fieldNames = static_cast<std::vector<std::string> *>(data);
    std::string fieldName(field);
    if (fieldName.substr(0, 8) == ("exif-ifd")) {
      fieldNames->push_back(fieldName);
    }
    return nullptr;
  }

  /*
    Remove all EXIF-related image fields.
  */
  VImage RemoveExif(VImage image) {
    std::vector<std::string> fieldNames;
    vips_image_map(image.get_image(), static_cast<VipsImageMapFn>(RemoveExifCallback), &fieldNames);
    for (const auto& f : fieldNames) {
      image.remove(f.data());
    }
    return image;
  }

  /*
    Get EXIF Orientation of image, if any.
  */
  int ExifOrientation(VImage image) {
    int orientation = 0;
    if (image.get_typeof(VIPS_META_ORIENTATION) != 0) {
      orientation = image.get_int(VIPS_META_ORIENTATION);
    }
    return orientation;
  }

  /*
    Set EXIF Orientation of image.
  */
  VImage SetExifOrientation(VImage image, int const orientation) {
    VImage copy = image.copy();
    copy.set(VIPS_META_ORIENTATION, orientation);
    return copy;
  }

  /*
    Remove EXIF Orientation from image.
  */
  VImage RemoveExifOrientation(VImage image) {
    VImage copy = image.copy();
    copy.remove(VIPS_META_ORIENTATION);
    copy.remove("exif-ifd0-Orientation");
    return copy;
  }

  /*
    Set animation properties if necessary.
  */
  VImage SetAnimationProperties(VImage image, int nPages, int pageHeight, std::vector<int> delay, int loop) {
    bool hasDelay = !delay.empty();
    VImage copy = image.copy();

    // Only set page-height if we have more than one page, or this could
    // accidentally turn into an animated image later.
    if (nPages > 1) copy.set(VIPS_META_PAGE_HEIGHT, pageHeight);
    if (hasDelay) {
      if (delay.size() == 1) {
        // We have just one delay, repeat that value for all frames.
        delay.insert(delay.end(), nPages - 1, delay[0]);
      }
      copy.set("delay", delay);
    }
    if (nPages == 1 && !hasDelay && loop == -1) {
      loop = 1;
    }
    if (loop != -1) copy.set("loop", loop);

    return copy;
  }

  /*
    Remove animation properties from image.
  */
  VImage RemoveAnimationProperties(VImage image) {
    VImage copy = image.copy();
    copy.remove(VIPS_META_PAGE_HEIGHT);
    copy.remove("delay");
    copy.remove("loop");
    return copy;
  }

  /*
    Remove GIF palette from image.
  */
  VImage RemoveGifPalette(VImage image) {
    VImage copy = image.copy();
    copy.remove("gif-palette");
    return copy;
  }

  /*
    Does this image have a non-default density?
  */
  bool HasDensity(VImage image) {
    return image.xres() > 1.0;
  }

  /*
    Get pixels/mm resolution as pixels/inch density.
  */
  int GetDensity(VImage image) {
    return static_cast<int>(round(image.xres() * 25.4));
  }

  /*
    Set pixels/mm resolution based on a pixels/inch density.
  */
  VImage SetDensity(VImage image, const double density) {
    const double pixelsPerMm = density / 25.4;
    VImage copy = image.copy();
    copy.get_image()->Xres = pixelsPerMm;
    copy.get_image()->Yres = pixelsPerMm;
    return copy;
  }

  /*
    Multi-page images can have a page height. Fetch it, and sanity check it.
    If page-height is not set, it defaults to the image height
  */
  int GetPageHeight(VImage image) {
    return vips_image_get_page_height(image.get_image());
  }

  /*
    Check the proposed format supports the current dimensions.
  */
  void AssertImageTypeDimensions(VImage image, ImageType const imageType) {
    const int height = image.get_typeof(VIPS_META_PAGE_HEIGHT) == G_TYPE_INT
      ? image.get_int(VIPS_META_PAGE_HEIGHT)
      : image.height();
    if (imageType == ImageType::JPEG) {
      if (image.width() > 65535 || height > 65535) {
        throw std::runtime_error("Processed image is too large for the JPEG format");
      }
    } else if (imageType == ImageType::WEBP) {
      if (image.width() > 16383 || height > 16383) {
        throw std::runtime_error("Processed image is too large for the WebP format");
      }
    } else if (imageType == ImageType::GIF) {
      if (image.width() > 65535 || height > 65535) {
        throw std::runtime_error("Processed image is too large for the GIF format");
      }
    } else if (imageType == ImageType::HEIF) {
      if (image.width() > 16384 || height > 16384) {
        throw std::runtime_error("Processed image is too large for the HEIF format");
      }
    }
  }

  /*
    Called when a Buffer undergoes GC, required to support mixed runtime libraries in Windows
  */
  std::function<void(void*, char*)> FreeCallback = [](void*, char* data) {
    g_free(data);
  };

  /*
    Temporary buffer of warnings
  */
  std::queue<std::string> vipsWarnings;
  std::mutex vipsWarningsMutex;

  /*
    Called with warnings from the glib-registered "VIPS" domain
  */
  void VipsWarningCallback(char const* log_domain, GLogLevelFlags log_level, char const* message, void* ignore) {
    std::lock_guard<std::mutex> lock(vipsWarningsMutex);
    vipsWarnings.emplace(message);
  }

  /*
    Pop the oldest warning message from the queue
  */
  std::string VipsWarningPop() {
    std::string warning;
    std::lock_guard<std::mutex> lock(vipsWarningsMutex);
    if (!vipsWarnings.empty()) {
      warning = vipsWarnings.front();
      vipsWarnings.pop();
    }
    return warning;
  }

  /*
    Attach an event listener for progress updates, used to detect timeout
  */
  void SetTimeout(VImage image, int const seconds) {
    if (seconds > 0) {
      VipsImage *im = image.get_image();
      if (im->progress_signal == NULL) {
        int *timeout = VIPS_NEW(im, int);
        *timeout = seconds;
        g_signal_connect(im, "eval", G_CALLBACK(VipsProgressCallBack), timeout);
        vips_image_set_progress(im, true);
      }
    }
  }

  /*
    Event listener for progress updates, used to detect timeout
  */
  void VipsProgressCallBack(VipsImage *im, VipsProgress *progress, int *timeout) {
    if (*timeout > 0 && progress->run >= *timeout) {
      vips_image_set_kill(im, true);
      vips_error("timeout", "%d%% complete", progress->percent);
      *timeout = 0;
    }
  }

  /*
    Calculate the (left, top) coordinates of the output image
    within the input image, applying the given gravity during an embed.

    @Azurebyte: We are basically swapping the inWidth and outWidth, inHeight and outHeight from the CalculateCrop function.
  */
  std::tuple<int, int> CalculateEmbedPosition(int const inWidth, int const inHeight,
    int const outWidth, int const outHeight, int const gravity) {

    int left = 0;
    int top = 0;
    switch (gravity) {
      case 1:
        // North
        left = (outWidth - inWidth) / 2;
        break;
      case 2:
        // East
        left = outWidth - inWidth;
        top = (outHeight - inHeight) / 2;
        break;
      case 3:
        // South
        left = (outWidth - inWidth) / 2;
        top = outHeight - inHeight;
        break;
      case 4:
        // West
        top = (outHeight - inHeight) / 2;
        break;
      case 5:
        // Northeast
        left = outWidth - inWidth;
        break;
      case 6:
        // Southeast
        left = outWidth - inWidth;
        top = outHeight - inHeight;
        break;
      case 7:
        // Southwest
        top = outHeight - inHeight;
        break;
      case 8:
        // Northwest
        // Which is the default is 0,0 so we do not assign anything here.
        break;
      default:
        // Centre
        left = (outWidth - inWidth) / 2;
        top = (outHeight - inHeight) / 2;
    }
    return std::make_tuple(left, top);
  }

  /*
    Calculate the (left, top) coordinates of the output image
    within the input image, applying the given gravity during a crop.
  */
  std::tuple<int, int> CalculateCrop(int const inWidth, int const inHeight,
    int const outWidth, int const outHeight, int const gravity) {

    int left = 0;
    int top = 0;
    switch (gravity) {
      case 1:
        // North
        left = (inWidth - outWidth + 1) / 2;
        break;
      case 2:
        // East
        left = inWidth - outWidth;
        top = (inHeight - outHeight + 1) / 2;
        break;
      case 3:
        // South
        left = (inWidth - outWidth + 1) / 2;
        top = inHeight - outHeight;
        break;
      case 4:
        // West
        top = (inHeight - outHeight + 1) / 2;
        break;
      case 5:
        // Northeast
        left = inWidth - outWidth;
        break;
      case 6:
        // Southeast
        left = inWidth - outWidth;
        top = inHeight - outHeight;
        break;
      case 7:
        // Southwest
        top = inHeight - outHeight;
        break;
      case 8:
        // Northwest
        break;
      default:
        // Centre
        left = (inWidth - outWidth + 1) / 2;
        top = (inHeight - outHeight + 1) / 2;
    }
    return std::make_tuple(left, top);
  }

  /*
    Calculate the (left, top) coordinates of the output image
    within the input image, applying the given x and y offsets.
  */
  std::tuple<int, int> CalculateCrop(int const inWidth, int const inHeight,
    int const outWidth, int const outHeight, int const x, int const y) {

    // default values
    int left = 0;
    int top = 0;

    // assign only if valid
    if (x < (inWidth - outWidth)) {
      left = x;
    } else if (x >= (inWidth - outWidth)) {
      left = inWidth - outWidth;
    }

    if (y < (inHeight - outHeight)) {
      top = y;
    } else if (y >= (inHeight - outHeight)) {
      top = inHeight - outHeight;
    }

    return std::make_tuple(left, top);
  }

  /*
    Are pixel values in this image 16-bit integer?
  */
  bool Is16Bit(VipsInterpretation const interpretation) {
    return interpretation == VIPS_INTERPRETATION_RGB16 || interpretation == VIPS_INTERPRETATION_GREY16;
  }

  /*
    Convert RGBA value to another colourspace
  */
  std::vector<double> GetRgbaAsColourspace(std::vector<double> const rgba,
    VipsInterpretation const interpretation, bool premultiply) {
    int const bands = static_cast<int>(rgba.size());
    if (bands < 3) {
      return rgba;
    }
    VImage pixel = VImage::new_matrix(1, 1);
    pixel.set("bands", bands);
    pixel = pixel
      .new_from_image(rgba)
      .colourspace(interpretation, VImage::option()->set("source_space", VIPS_INTERPRETATION_sRGB));
    if (premultiply) {
      pixel = pixel.premultiply();
    }
    return pixel(0, 0);
  }

  /*
    Apply the alpha channel to a given colour
  */
  std::tuple<VImage, std::vector<double>> ApplyAlpha(VImage image, std::vector<double> colour, bool premultiply) {
    // Scale up 8-bit values to match 16-bit input image
    double const multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0;
    // Create alphaColour colour
    std::vector<double> alphaColour;
    if (image.bands() > 2) {
      alphaColour = {
        multiplier * colour[0],
        multiplier * colour[1],
        multiplier * colour[2]
      };
    } else {
      // Convert sRGB to greyscale
      alphaColour = { multiplier * (
        0.2126 * colour[0] +
        0.7152 * colour[1] +
        0.0722 * colour[2])
      };
    }
    // Add alpha channel(s) to alphaColour colour
    if (colour[3] < 255.0 || image.has_alpha()) {
      int extraBands = image.bands() > 4 ? image.bands() - 3 : 1;
      alphaColour.insert(alphaColour.end(), extraBands, colour[3] * multiplier);
    }
    // Ensure alphaColour colour uses correct colourspace
    alphaColour = sharp::GetRgbaAsColourspace(alphaColour, image.interpretation(), premultiply);
    // Add non-transparent alpha channel, if required
    if (colour[3] < 255.0 && !image.has_alpha()) {
      image = image.bandjoin_const({ 255 * multiplier });
    }
    return std::make_tuple(image, alphaColour);
  }

  /*
    Removes alpha channels, if any.
  */
  VImage RemoveAlpha(VImage image) {
    while (image.bands() > 1 && image.has_alpha()) {
      image = image.extract_band(0, VImage::option()->set("n", image.bands() - 1));
    }
    return image;
  }

  /*
    Ensures alpha channel, if missing.
  */
  VImage EnsureAlpha(VImage image, double const value) {
    if (!image.has_alpha()) {
      image = image.bandjoin_const({ value * vips_interpretation_max_alpha(image.interpretation()) });
    }
    return image;
  }

  std::pair<double, double> ResolveShrink(int width, int height, int targetWidth, int targetHeight,
    Canvas canvas, bool withoutEnlargement, bool withoutReduction) {
    double hshrink = 1.0;
    double vshrink = 1.0;

    if (targetWidth > 0 && targetHeight > 0) {
      // Fixed width and height
      hshrink = static_cast<double>(width) / targetWidth;
      vshrink = static_cast<double>(height) / targetHeight;

      switch (canvas) {
        case Canvas::CROP:
        case Canvas::MIN:
          if (hshrink < vshrink) {
            vshrink = hshrink;
          } else {
            hshrink = vshrink;
          }
          break;
        case Canvas::EMBED:
        case Canvas::MAX:
          if (hshrink > vshrink) {
            vshrink = hshrink;
          } else {
            hshrink = vshrink;
          }
          break;
        case Canvas::IGNORE_ASPECT:
          break;
      }
    } else if (targetWidth > 0) {
      // Fixed width
      hshrink = static_cast<double>(width) / targetWidth;

      if (canvas != Canvas::IGNORE_ASPECT) {
        // Auto height
        vshrink = hshrink;
      }
    } else if (targetHeight > 0) {
      // Fixed height
      vshrink = static_cast<double>(height) / targetHeight;

      if (canvas != Canvas::IGNORE_ASPECT) {
        // Auto width
        hshrink = vshrink;
      }
    }

    // We should not reduce or enlarge the output image, if
    // withoutReduction or withoutEnlargement is specified.
    if (withoutReduction) {
      // Equivalent of VIPS_SIZE_UP
      hshrink = std::min(1.0, hshrink);
      vshrink = std::min(1.0, vshrink);
    } else if (withoutEnlargement) {
      // Equivalent of VIPS_SIZE_DOWN
      hshrink = std::max(1.0, hshrink);
      vshrink = std::max(1.0, vshrink);
    }

    // We don't want to shrink so much that we send an axis to 0
    hshrink = std::min(hshrink, static_cast<double>(width));
    vshrink = std::min(vshrink, static_cast<double>(height));

    return std::make_pair(hshrink, vshrink);
  }

  /*
    Ensure decoding remains sequential.
  */
  VImage StaySequential(VImage image, bool condition) {
    if (vips_image_is_sequential(image.get_image()) && condition) {
      image = image.copy_memory().copy();
      image.remove(VIPS_META_SEQUENTIAL);
    }
    return image;
  }

  /*
    Does this image have a gain map?
  */
  bool HasGainMap(VImage image) {
    return image.get_typeof("gainmap") == VIPS_TYPE_BLOB ||
      image.get_typeof("gainmap-data") == VIPS_TYPE_BLOB;
  }

  /*
    Removes gain map, if any.
  */
  VImage RemoveGainMap(VImage image) {
    VImage copy = image.copy();
    copy.remove("gainmap");
    copy.remove("gainmap-data");
    return copy;
  }
}  // namespace sharp
