#include <cstdlib>
#include <string>
#include <string.h>

#include <node.h>
#include <node_buffer.h>
#include <vips/vips8>

#include "nan.h"
#include "common.h"

using vips::VImage;

namespace sharp {

  // Convenience methods to access the attributes of a v8::Object
  bool HasAttr(v8::Handle<v8::Object> obj, std::string attr) {
    return Nan::Has(obj, Nan::New(attr).ToLocalChecked()).FromJust();
  }
  std::string AttrAsStr(v8::Handle<v8::Object> obj, std::string attr) {
    return *Nan::Utf8String(Nan::Get(obj, Nan::New(attr).ToLocalChecked()).ToLocalChecked());
  }

  // Create an InputDescriptor instance from a v8::Object describing an input image
  InputDescriptor* CreateInputDescriptor(
    v8::Handle<v8::Object> input, std::vector<v8::Local<v8::Object>> buffersToPersist
  ) {
    Nan::HandleScope();
    InputDescriptor *descriptor = new InputDescriptor;
    if (HasAttr(input, "file")) {
      descriptor->file = AttrAsStr(input, "file");
    } else {
      v8::Local<v8::Object> buffer = AttrAs<v8::Object>(input, "buffer");
      descriptor->bufferLength = node::Buffer::Length(buffer);
      descriptor->buffer = node::Buffer::Data(buffer);
      buffersToPersist.push_back(buffer);
    }
    // Density for vector-based input
    if (HasAttr(input, "density")) {
      descriptor->density = AttrTo<uint32_t>(input, "density");
    }
    // Raw pixel input
    if (HasAttr(input, "rawChannels")) {
      descriptor->rawChannels = AttrTo<uint32_t>(input, "rawChannels");
      descriptor->rawWidth = AttrTo<uint32_t>(input, "rawWidth");
      descriptor->rawHeight = AttrTo<uint32_t>(input, "rawHeight");
    }
    return descriptor;
  }

  // How many tasks are in the queue?
  volatile int counterQueue = 0;

  // How many tasks are being processed?
  volatile 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 IsTiff(std::string const &str) {
    return EndsWith(str, ".tif") || EndsWith(str, ".tiff") || EndsWith(str, ".TIF") || EndsWith(str, ".TIFF");
  }
  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");
  }

  /*
    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::SVG: id = "svg"; 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::VIPS: id = "v"; break;
      case ImageType::RAW: id = "raw"; break;
      case ImageType::UNKNOWN: id = "unknown"; break;
    }
    return id;
  }

  /*
    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 != NULL) {
      std::string const loader = load;
      if (EndsWith(loader, "JpegBuffer")) {
        imageType = ImageType::JPEG;
      } else if (EndsWith(loader, "PngBuffer")) {
        imageType = ImageType::PNG;
      } else if (EndsWith(loader, "WebpBuffer")) {
        imageType = ImageType::WEBP;
      } else if (EndsWith(loader, "TiffBuffer")) {
        imageType = ImageType::TIFF;
      } else if (EndsWith(loader, "GifBuffer")) {
        imageType = ImageType::GIF;
      } else if (EndsWith(loader, "SvgBuffer")) {
        imageType = ImageType::SVG;
      } else if (EndsWith(loader, "PdfBuffer")) {
        imageType = ImageType::PDF;
      } else if (EndsWith(loader, "MagickBuffer")) {
        imageType = ImageType::MAGICK;
      }
    }
    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) {
      std::string const loader = load;
      if (EndsWith(loader, "JpegFile")) {
        imageType = ImageType::JPEG;
      } else if (EndsWith(loader, "Png")) {
        imageType = ImageType::PNG;
      } else if (EndsWith(loader, "WebpFile")) {
        imageType = ImageType::WEBP;
      } else if (EndsWith(loader, "Openslide")) {
        imageType = ImageType::OPENSLIDE;
      } else if (EndsWith(loader, "TiffFile")) {
        imageType = ImageType::TIFF;
      } else if (EndsWith(loader, "GifFile")) {
        imageType = ImageType::GIF;
      } else if (EndsWith(loader, "SvgFile")) {
        imageType = ImageType::SVG;
      } else if (EndsWith(loader, "PdfFile")) {
        imageType = ImageType::PDF;
      } else if (EndsWith(loader, "Ppm")) {
        imageType = ImageType::PPM;
      } else if (EndsWith(loader, "Fits")) {
        imageType = ImageType::FITS;
      } else if (EndsWith(loader, "Vips")) {
        imageType = ImageType::VIPS;
      } else if (EndsWith(loader, "Magick") || EndsWith(loader, "MagickFile")) {
        imageType = ImageType::MAGICK;
      }
    }
    return imageType;
  }

  /*
    Open an image from the given InputDescriptor (filesystem, compressed buffer, raw pixel data)
  */
  std::tuple<VImage, ImageType> OpenInput(InputDescriptor *descriptor, VipsAccess accessMethod) {
    VImage image;
    ImageType imageType;
    if (descriptor->buffer != nullptr) {
      // From buffer
      if (descriptor->rawChannels > 0) {
        // Raw, uncompressed pixel data
        image = VImage::new_from_memory(descriptor->buffer, descriptor->bufferLength,
          descriptor->rawWidth, descriptor->rawHeight, descriptor->rawChannels, VIPS_FORMAT_UCHAR);
        if (descriptor->rawChannels < 3) {
          image.get_image()->Type = VIPS_INTERPRETATION_B_W;
        } else {
          image.get_image()->Type = VIPS_INTERPRETATION_sRGB;
        }
        imageType = ImageType::RAW;
      } else {
        // Compressed data
        imageType = DetermineImageType(descriptor->buffer, descriptor->bufferLength);
        if (imageType != ImageType::UNKNOWN) {
          try {
            vips::VOption *option = VImage::option()->set("access", accessMethod);
            if (imageType == ImageType::SVG || imageType == ImageType::PDF) {
              option->set("dpi", static_cast<double>(descriptor->density));
            }
            if (imageType == ImageType::MAGICK) {
              option->set("density", std::to_string(descriptor->density).data());
            }
            image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option);
            if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
              SetDensity(image, descriptor->density);
            }
          } catch (...) {
            throw vips::VError("Input buffer has corrupt header");
          }
        } else {
          throw vips::VError("Input buffer contains unsupported image format");
        }
      }
    } else {
      // From filesystem
      imageType = DetermineImageType(descriptor->file.data());
      if (imageType != ImageType::UNKNOWN) {
        try {
          vips::VOption *option = VImage::option()->set("access", accessMethod);
          if (imageType == ImageType::SVG || imageType == ImageType::PDF) {
            option->set("dpi", static_cast<double>(descriptor->density));
          }
          if (imageType == ImageType::MAGICK) {
            option->set("density", std::to_string(descriptor->density).data());
          }
          image = VImage::new_from_file(descriptor->file.data(), option);
          if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
            SetDensity(image, descriptor->density);
          }
        } catch (...) {
          throw vips::VError("Input file has corrupt header");
        }
      } else {
        throw vips::VError("Input file is missing or of an unsupported image format");
      }
    }
    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) != 0) ? TRUE : FALSE;
  }

  /*
    Does this image have an alpha channel?
    Uses colour space interpretation with number of channels to guess this.
  */
  bool HasAlpha(VImage image) {
    int const bands = image.bands();
    VipsInterpretation const interpretation = image.interpretation();
    return (
      (bands == 2 && interpretation == VIPS_INTERPRETATION_B_W) ||
      (bands == 4 && interpretation != VIPS_INTERPRETATION_CMYK) ||
      (bands == 5 && interpretation == VIPS_INTERPRETATION_CMYK)
    );
  }

  /*
    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.
  */
  void SetExifOrientation(VImage image, int const orientation) {
    image.set(VIPS_META_ORIENTATION, orientation);
  }

  /*
    Remove EXIF Orientation from image.
  */
  void RemoveExifOrientation(VImage image) {
    vips_image_remove(image.get_image(), VIPS_META_ORIENTATION);
  }

  /*
    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.
  */
  void SetDensity(VImage image, const int density) {
    const double pixelsPerMm = static_cast<double>(density) / 25.4;
    image.set("Xres", pixelsPerMm);
    image.set("Yres", pixelsPerMm);
    image.set(VIPS_META_RESOLUTION_UNIT, "in");
  }

  /*
    Called when a Buffer undergoes GC, required to support mixed runtime libraries in Windows
  */
  void FreeCallback(char* data, void* hint) {
    if (data != nullptr) {
      g_free(data);
    }
  }

  /*
    Calculate the (left, top) coordinates of the output image
    within the input image, applying the given gravity.
  */
  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;
      case 7:
        // Southwest
        top = inHeight - outHeight;
      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 >= 0 && x < (inWidth - outWidth)) {
      left = x;
    } else if(x >= (inWidth - outWidth)) {
      left = inWidth - outWidth;
    }

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

    // the resulting left and top could have been outside the image after calculation from bottom/right edges
    if(left < 0) {
      left = 0;
    }
    if(top < 0) {
      top = 0;
    }

    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;
  }

  /*
    Return the image alpha maximum. Useful for combining alpha bands. scRGB
    images are 0 - 1 for image data, but the alpha is 0 - 255.
  */
  double MaximumImageAlpha(VipsInterpretation const interpretation) {
    return Is16Bit(interpretation) ? 65535.0 : 255.0;
  }

  /*
    Get boolean operation type from string
  */
  VipsOperationBoolean GetBooleanOperation(std::string const opStr) {
    return static_cast<VipsOperationBoolean>(
      vips_enum_from_nick(nullptr, VIPS_TYPE_OPERATION_BOOLEAN, opStr.data())
    );
  }

  /*
    Get interpretation type from string
  */
  VipsInterpretation GetInterpretation(std::string const typeStr) {
    return static_cast<VipsInterpretation>(
      vips_enum_from_nick(nullptr, VIPS_TYPE_INTERPRETATION, typeStr.data())
    );
  }

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

}  // namespace sharp
