/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * This program 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 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <string>

#include <lcms2.h>

/*  These libgimp includes are not needed here at all, but this is a
 *  convenient place to make sure the public libgimp headers are
 *  C++-clean. The C++ compiler will choke on stuff like naming
 *  a struct member or parameter "private".
 */
#include "libgimp/gimp.h"
#include "libgimp/gimpui.h"
#include "libgimpbase/gimpbase.h"
#include "libgimpmath/gimpmath.h"
#include "libgimpcolor/gimpcolor.h"
#include "libgimpconfig/gimpconfig.h"
#include "libgimpmodule/gimpmodule.h"
#include "libgimpthumb/gimpthumb.h"
#include "libgimpwidgets/gimpwidgets.h"

#if defined(__MINGW32__)
#ifndef FLT_EPSILON
#define FLT_EPSILON  __FLT_EPSILON__
#endif
#ifndef DBL_EPSILON
#define DBL_EPSILON  __DBL_EPSILON__
#endif
#ifndef LDBL_EPSILON
#define LDBL_EPSILON __LDBL_EPSILON__
#endif
#endif

/* ignore deprecated warnings from OpenEXR headers */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated"
#include <ImfInputFile.h>
#include <ImfChannelList.h>
#include <ImfRgbaFile.h>
#include <ImfArray.h>
#include <ImfRgbaYca.h>
#include <ImfStandardAttributes.h>
#pragma GCC diagnostic pop

#include "exr-attribute-blob.h"
#include "openexr-wrapper.h"

using namespace Imf;
using namespace Imf::RgbaYca;
using namespace Imath;

static bool XYZ_equal (cmsCIEXYZ *a, cmsCIEXYZ *b)
{
  static const double epsilon = 0.0001;
  /* Y is encoding the luminance, we normalize that for comparison */
  return fabs ((a->X / a->Y * b->Y) - b->X) < epsilon &&
         fabs ((a->Y / a->Y * b->Y) - b->Y) < epsilon &&
         fabs ((a->Z / a->Y * b->Y) - b->Z) < epsilon;
}

struct _EXRLoader
{
  _EXRLoader (const char* filename) :
    refcount_ (1),
    file_ (filename),
    rgbaInputFile_ (filename),
    data_window_ (file_.header ().dataWindow ()),
    channels_ (file_.header ().channels ())
  {
    std::set<string> layerNames;
    bool             loaded;

    channels_.layers (layerNames);
    layers_only_ = false;
    layer_count_ = layerNames.size ();

    loaded = initializeImage (false);

    /* The OpenEXR image may have named layers only, not loose channels.
     * In that case, we need to go through each layer name and append it
     * to the call for findChannel () */
    if (! loaded)
      {
        layers_only_ = true;

        for (std::set<std::string>::const_iterator i = layerNames.begin ();
             i != layerNames.end (); ++i)
          {
            loaded = initializeImage (true, *i + ".");

            if (loaded)
              break;
          }
      }
  }

  bool initializeImage (bool               has_layers,
                        const std::string &prefix = "")
  {
    const Channel* chan;

    can_load_ = true;

    /* Capitalization matters - channel "R" and "r" are both red,
     * but unless you use the specific one mentioned in the file,
     * it won't load. */
    if (channels_.findChannel (prefix + "R") ||
        channels_.findChannel (prefix + "G") ||
        channels_.findChannel (prefix + "B") ||
        channels_.findChannel (prefix + "r") ||
        channels_.findChannel (prefix + "g") ||
        channels_.findChannel (prefix + "b"))
      {
        format_string_ = "RGB";
        image_type_ = IMAGE_TYPE_RGB;

        if ((chan = channels_.findChannel (prefix + "R")) ||
            (chan = channels_.findChannel (prefix + "r")))
          pt_ = chan->type;
        else if ((chan = channels_.findChannel (prefix + "G")) ||
                 (chan = channels_.findChannel (prefix + "g")))
          pt_ = chan->type;
        else if ((chan = channels_.findChannel (prefix + "B")) ||
                 (chan = channels_.findChannel (prefix + "b")))
          pt_ = chan->type;
      }
    else if (channels_.findChannel (prefix + "Y") &&
             (channels_.findChannel (prefix + "RY") ||
              channels_.findChannel (prefix + "BY")))
      {
        format_string_ = "RGB";
        image_type_ = IMAGE_TYPE_YUV;

        pt_ = channels_.findChannel (prefix + "Y")->type;
      }
    else if (channels_.findChannel (prefix + "Y") ||
             channels_.findChannel (prefix + "y"))
      {
        format_string_ = "Y";
        image_type_ = IMAGE_TYPE_GRAY;

        if ((chan = channels_.findChannel (prefix + "Y")) ||
            (chan = channels_.findChannel (prefix + "y")))
          pt_ = chan->type;
      }
    else
      {
        int         channel_count = 0;
        const char *channel_name  = NULL;

        for (ChannelList::ConstIterator i = channels_.begin ();
             i != channels_.end (); ++i)
          {
            channel_count++;

            pt_ = i.channel ().type;
            channel_name = i.name ();
          }

       /* Assume single channel images are grayscale,
        * no matter what the channel name is. */
        if (channel_count == 1)
          {
            format_string_ = channel_name;
            image_type_ = IMAGE_TYPE_UNKNOWN_1_CHANNEL;
            unknown_channel_name_ = channel_name;

            /* TODO: Pass this information back so it can be displayed
             * in the UI. */
            printf ("OpenEXR Warning: Single channel image with unknown "
                    "channel %s, loading as grayscale\n", channel_name);
          }
        else
          {
            /* TODO: After string freeze ends add unsupported type notice. */
            can_load_ = false;
          }
      }

    if (channels_.findChannel (prefix + "A"))
      {
        format_string_.append (prefix + "A");
        has_alpha_ = true;
      }
    else if (channels_.findChannel (prefix + "a"))
      {
        format_string_.append (prefix + "a");
        has_alpha_ = true;
      }
    else
      {
        has_alpha_ = false;
      }

    switch (pt_)
      {
      case UINT:
        format_string_.append (" u32");
        bpc_ = 4;
        break;
      case HALF:
        format_string_.append (" half");
        bpc_ = 2;
        break;
      case FLOAT:
      default:
        format_string_.append (" float");
        bpc_ = 4;
      }

    return can_load_;
  }

  int readPixelRow (char *pixels,
                   int   bpp,
                   int   row,
                   int   layer_index)
  {
    const int        actual_row = data_window_.min.y + row;
    FrameBuffer      fb;
    std::set<string> layerNames;
    std::string      prefix     = "";
    /* This is necessary because OpenEXR expects the buffer to begin at
     * (0, 0). Though it probably results in some unmapped address,
     * hopefully OpenEXR will not make use of it. :/ */
    char* base = pixels - (data_window_.min.x * bpp);

    if (layer_index > -1)
      {
        channels_.layers (layerNames);

        prefix = *std::next (layerNames.begin (), layer_index) + ".";
      }

    switch (image_type_)
      {
      case IMAGE_TYPE_UNKNOWN_1_CHANNEL:
        fb.insert (unknown_channel_name_, Slice (pt_, base, bpp, 0, 1, 1, 0.5));
        break;

      case IMAGE_TYPE_YUV:
        {
           int width = getWidth();
           Array<Rgba> row_buffer (width);

           rgbaInputFile_.setFrameBuffer (&row_buffer[0] - data_window_.min.x,
                                          1,
                                          0);

          rgbaInputFile_.readPixels (actual_row, actual_row);

          for (int i = 0; i < width; i++)
            {
              half *pixel = (half *)(base + (i * bpp));
              pixel[0] = row_buffer[i].r;
              pixel[1] = row_buffer[i].g;
              pixel[2] = row_buffer[i].b;
              if (hasAlpha ())
                {
                  pixel[3] = row_buffer[i].a;
                }
            }
            /* return early to skip the file_.setFrameBuffer() and
             * file_.readPixels() calls below, which are used by the
             * RGB and GRAY cases.
             * YUV reading is handled entirely by rgbaInputFile_ above. */
            return 0;
        }
      case IMAGE_TYPE_GRAY:
        if (channels_.findChannel (prefix + "Y"))
          {
            fb.insert (prefix + "Y", Slice (pt_, base, bpp, 0, 1, 1, 0.5));
            if (hasAlpha ())
              {
                fb.insert (prefix + "A", Slice (pt_, base + bpc_, bpp, 0, 1, 1, 1.0));
              }
          }
        else
          {
            {
              fb.insert (prefix + "y", Slice (pt_, base, bpp, 0, 1, 1, 0.5));
              if (hasAlpha())
                {
                  fb.insert (prefix + "a", Slice (pt_, base + bpc_, bpp, 0, 1, 1, 1.0));
                }
            }
          }
        break;

      case IMAGE_TYPE_RGB:
      default:
        if (channels_.findChannel (prefix + "R"))
          {
            fb.insert (prefix + "R", Slice (pt_, base + (bpc_ * 0), bpp, 0, 1, 1, 0.0));
            fb.insert (prefix + "G", Slice (pt_, base + (bpc_ * 1), bpp, 0, 1, 1, 0.0));
            fb.insert (prefix + "B", Slice (pt_, base + (bpc_ * 2), bpp, 0, 1, 1, 0.0));
            if (hasAlpha ())
              {
                fb.insert (prefix + "A", Slice (pt_, base + (bpc_ * 3), bpp, 0, 1, 1, 1.0));
              }
          }
        else
          {
            fb.insert (prefix + "r", Slice (pt_, base + (bpc_ * 0), bpp, 0, 1, 1, 0.0));
            fb.insert (prefix + "g", Slice (pt_, base + (bpc_ * 1), bpp, 0, 1, 1, 0.0));
            fb.insert (prefix + "b", Slice (pt_, base + (bpc_ * 2), bpp, 0, 1, 1, 0.0));
            if (hasAlpha ())
              {
                fb.insert (prefix + "a", Slice (pt_, base + (bpc_ * 3), bpp, 0, 1, 1, 1.0));
              }
          }
      }

    file_.setFrameBuffer (fb);
    file_.readPixels (actual_row);

    return 0;
  }

  int getWidth () const {
    return data_window_.max.x - data_window_.min.x + 1;
  }

  int getHeight () const {
    return data_window_.max.y - data_window_.min.y + 1;
  }

  EXRPrecision getPrecision () const {
    EXRPrecision prec;

    switch (pt_)
      {
      case UINT:
        prec = PREC_UINT;
        break;
      case HALF:
        prec = PREC_HALF;
        break;
      case FLOAT:
      default:
        prec = PREC_FLOAT;
      }

    return prec;
  }

  EXRImageType getImageType () const {
    return image_type_;
  }

  int hasAlpha () const {
    return has_alpha_ ? 1 : 0;
  }

  int canLoad () const {
    return can_load_ ? 1 : 0;
  }

  GimpColorProfile *getProfile () const {
    Chromaticities chromaticities;
    float whiteLuminance = 1.0;

    GimpColorProfile *linear_srgb_profile;
    cmsHPROFILE linear_srgb_lcms;

    GimpColorProfile *profile;
    cmsHPROFILE lcms_profile;

    cmsCIEXYZ *gimp_r_XYZ, *gimp_g_XYZ, *gimp_b_XYZ, *gimp_w_XYZ;
    cmsCIEXYZ exr_r_XYZ, exr_g_XYZ, exr_b_XYZ, exr_w_XYZ;

    /* get the color information from the EXR */
    if (hasChromaticities (file_.header ()))
      chromaticities = Imf::chromaticities (file_.header ());
    else
      return NULL;

    if (Imf::hasWhiteLuminance (file_.header ()))
      whiteLuminance = Imf::whiteLuminance (file_.header ());
    else
      return NULL;

#if 0
    std::cout << "hasChromaticities: "
              << hasChromaticities (file_.header ())
              << std::endl;
    std::cout << "hasWhiteLuminance: "
              << hasWhiteLuminance (file_.header ())
              << std::endl;
    std::cout << whiteLuminance << std::endl;
    std::cout << chromaticities.red << std::endl;
    std::cout << chromaticities.green << std::endl;
    std::cout << chromaticities.blue << std::endl;
    std::cout << chromaticities.white << std::endl;
    std::cout << std::endl;
#endif

    cmsCIExyY whitePoint = { chromaticities.white.x,
                             chromaticities.white.y,
                             whiteLuminance };
    cmsCIExyYTRIPLE CameraPrimaries = { { chromaticities.red.x,
                                          chromaticities.red.y,
                                          whiteLuminance },
                                        { chromaticities.green.x,
                                          chromaticities.green.y,
                                          whiteLuminance },
                                        { chromaticities.blue.x,
                                          chromaticities.blue.y,
                                          whiteLuminance } };

    /* get the primaries + wp from GIMP's internal linear sRGB profile */
    linear_srgb_profile = gimp_color_profile_new_rgb_srgb_linear ();
    linear_srgb_lcms = gimp_color_profile_get_lcms_profile (linear_srgb_profile);

    gimp_r_XYZ = (cmsCIEXYZ *) cmsReadTag (linear_srgb_lcms, cmsSigRedColorantTag);
    gimp_g_XYZ = (cmsCIEXYZ *) cmsReadTag (linear_srgb_lcms, cmsSigGreenColorantTag);
    gimp_b_XYZ = (cmsCIEXYZ *) cmsReadTag (linear_srgb_lcms, cmsSigBlueColorantTag);
    gimp_w_XYZ = (cmsCIEXYZ *) cmsReadTag (linear_srgb_lcms, cmsSigMediaWhitePointTag);

    cmsxyY2XYZ (&exr_r_XYZ, &CameraPrimaries.Red);
    cmsxyY2XYZ (&exr_g_XYZ, &CameraPrimaries.Green);
    cmsxyY2XYZ (&exr_b_XYZ, &CameraPrimaries.Blue);
    cmsxyY2XYZ (&exr_w_XYZ, &whitePoint);

    /* ... and check if the data stored in the EXR matches GIMP's internal profile */
    bool exr_is_linear_srgb = XYZ_equal (&exr_r_XYZ, gimp_r_XYZ) &&
                              XYZ_equal (&exr_g_XYZ, gimp_g_XYZ) &&
                              XYZ_equal (&exr_b_XYZ, gimp_b_XYZ) &&
                              XYZ_equal (&exr_w_XYZ, gimp_w_XYZ);

    /* using GIMP's linear sRGB profile allows to skip the conversion popup */
    if (exr_is_linear_srgb)
      return linear_srgb_profile;

    /* nope, it's something else. Clean up and build a new profile */
    g_object_unref (linear_srgb_profile);

    /* TODO: maybe factor this out into libgimpcolor/gimpcolorprofile.h ? */
    double Parameters[2] = { 1.0, 0.0 };
    cmsToneCurve *Gamma[3];
    Gamma[0] = Gamma[1] = Gamma[2] = cmsBuildParametricToneCurve(0,
                                                                 1,
                                                                 Parameters);
    lcms_profile = cmsCreateRGBProfile (&whitePoint, &CameraPrimaries, Gamma);
    cmsFreeToneCurve (Gamma[0]);
    if (lcms_profile == NULL) return NULL;

    /* cmsSetProfileVersion (lcms_profile, 2.1); */
    cmsMLU *mlu0 = cmsMLUalloc (NULL, 1);
    cmsMLUsetASCII (mlu0, "en", "US", "(GIMP internal)");
    cmsMLU *mlu1 = cmsMLUalloc(NULL, 1);
    cmsMLUsetASCII (mlu1, "en", "US", "color profile from EXR chromaticities");
    cmsMLU *mlu2 = cmsMLUalloc(NULL, 1);
    cmsMLUsetASCII (mlu2, "en", "US", "color profile from EXR chromaticities");
    cmsWriteTag (lcms_profile, cmsSigDeviceMfgDescTag, mlu0);
    cmsWriteTag (lcms_profile, cmsSigDeviceModelDescTag, mlu1);
    cmsWriteTag (lcms_profile, cmsSigProfileDescriptionTag, mlu2);
    cmsMLUfree (mlu0);
    cmsMLUfree (mlu1);
    cmsMLUfree (mlu2);

    profile = gimp_color_profile_new_from_lcms_profile (lcms_profile,
                                                        NULL);
    cmsCloseProfile (lcms_profile);

    return profile;
  }

  gchar *getComment () const {
    char *result = NULL;
    const Imf::StringAttribute *comment = file_.header ().findTypedAttribute<Imf::StringAttribute>("comment");
    if (comment)
      result = g_strdup (comment->value().c_str());
    return result;
  }

  guchar *getExif (guint *size) const {
    guchar jpeg_exif[] = "Exif\0\0";
    guchar *exif_data = NULL;
    *size = 0;

    const Imf::BlobAttribute *exif = file_.header ().findTypedAttribute<Imf::BlobAttribute>("exif");

    if (exif)
      {
        exif_data = (guchar *)(exif->value ().data.get ());
        *size = exif->value ().size;
        /* darktable 4.0.0 and earlier appended a jpg-compatible exif00 string,
         * so get rid of that again. We explicitly reduce the size by 1 since
         * the compiler adds an extra \0 to the end of jpeg_exif. */
        if ( ! memcmp (jpeg_exif, exif_data, sizeof (jpeg_exif)-1))
          {
            *size -= 6;
            exif_data += 6;
          }
      }

    return (guchar *)g_memdup2 (exif_data, *size);
  }

  guchar *getXmp (guint *size) const {
    guchar *result = NULL;
    *size = 0;
    const Imf::StringAttribute *xmp = file_.header ().findTypedAttribute<Imf::StringAttribute>("xmp");
    if (xmp)
      {
        *size = xmp->value ().size ();
        result = (guchar *) g_memdup2 (xmp->value().data(), *size);
      }
    return result;
  }

  gchar *getLayerName (int index) const {
    gchar *result = NULL;

    if (index > -1 && index < layer_count_)
      {
        std::set<string> layerNames;
        std::string      name;

        channels_.layers (layerNames);

        name   = *std::next (layerNames.begin(), index);
        result = (gchar *) g_memdup2 (name.c_str(),
                                      name.size() + 1);
      }
    return result;
  }

  int getLayerInfo (gint *num_layers, int *layers_only) const {
    *num_layers  = layer_count_;
    *layers_only = layers_only_;

    return 1;
  }

  size_t refcount_;
  InputFile file_;
  RgbaInputFile rgbaInputFile_;
  const Box2i data_window_;
  const ChannelList& channels_;
  PixelType pt_;
  int bpc_;
  EXRImageType image_type_;
  bool has_alpha_;
  bool layers_only_;
  int  layer_count_;
  bool can_load_;
  std::string format_string_;
  std::string unknown_channel_name_;
};

EXRLoader*
exr_loader_new (const char *filename)
{
  EXRLoader* file;

  /* Don't let any exceptions propagate to the C layer. */
  try
    {
      Imf::BlobAttribute::registerAttributeType();
      file = new EXRLoader (filename);

      if (file && ! file->canLoad ())
        {
          exr_loader_unref (file);
          file = NULL;
        }
    }
  catch (...)
    {
      file = NULL;
    }

  return file;
}

EXRLoader*
exr_loader_ref (EXRLoader *loader)
{
  ++loader->refcount_;
  return loader;
}

void
exr_loader_unref (EXRLoader *loader)
{
  if (--loader->refcount_ == 0)
    {
      delete loader;
    }
}

int
exr_loader_get_width (EXRLoader *loader)
{
  int width;
  /* Don't let any exceptions propagate to the C layer. */
  try
    {
      width = loader->getWidth ();
    }
  catch (...)
    {
      width = -1;
    }

  return width;
}

int
exr_loader_get_height (EXRLoader *loader)
{
  int height;
  /* Don't let any exceptions propagate to the C layer. */
  try
    {
      height = loader->getHeight ();
    }
  catch (...)
    {
      height = -1;
    }

  return height;
}

EXRImageType
exr_loader_get_image_type (EXRLoader *loader)
{
  /* This does not throw. */
  return loader->getImageType ();
}

EXRPrecision
exr_loader_get_precision (EXRLoader *loader)
{
  /* This does not throw. */
  return loader->getPrecision ();
}

int
exr_loader_has_alpha (EXRLoader *loader)
{
  /* This does not throw. */
  return loader->hasAlpha ();
}

GimpColorProfile *
exr_loader_get_profile (EXRLoader *loader)
{
  return loader->getProfile ();
}

gchar *
exr_loader_get_comment (EXRLoader *loader)
{
  return loader->getComment ();
}

guchar *
exr_loader_get_exif (EXRLoader *loader,
                     guint *size)
{
  return loader->getExif (size);
}

guchar *
exr_loader_get_xmp (EXRLoader *loader,
                    guint *size)
{
  return loader->getXmp (size);
}

gchar *
exr_loader_get_layer_name (EXRLoader  *loader,
                           gint        index)
{
  return loader->getLayerName (index);
}

int
exr_loader_get_layer_info (EXRLoader *loader,
                           gint      *num_layers,
                           gboolean  *layers_only)
{
  return loader->getLayerInfo (num_layers, layers_only);
}

int
exr_loader_read_pixel_row (EXRLoader *loader,
                           char      *pixels,
                           int        bpp,
                           int        row,
                           int        layer_index)
{
  int retval = -1;
  /* Don't let any exceptions propagate to the C layer. */
  try
    {
      retval = loader->readPixelRow (pixels, bpp, row, layer_index);
    }
  catch (...)
    {
      retval = -1;
    }

  return retval;
}
