/* 
 * OpenEXR RGBA file plug-in for filmgimp.
 * Based loosely on the Cineon and SGI plug-ins.
 *
 * Copyright (C) 2002 Drew Hess <dhess@ilm.com>
 *
 * 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 2 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, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/* 
 * OpenEXR images are typically linear.  Filmgimp needs a "display gamma"
 * concept so that linear images can be displayed properly on a CRT or
 * LCD.  For now, however, you actually have to set the gamma of the
 * image using the Gamma/Expose control if you want the image to
 * look "right."  Don't forget to invert the gamma before you write the 
 * image back out (should be written out as linear data).
 */

#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ImfRgbaFile.h>
#include <ImfInputFile.h>
#include <ImfFrameBuffer.h>
#include <Iex.h>
#include <ImathBox.h>
#include <half.h>
#include "gtk/gtk.h"
extern "C" {
#include "lib/plugin_main.h"
#include "lib/wire/libtile.h"
#include "plugin_pdb.h"
}

#ifdef __cplusplus
extern "C" {
#endif

static void   query      (void);
static void   run        (char    *name,
			  int      nparams,
			  GParam  *param,
			  int     *nreturn_vals,
			  GParam **return_vals);
static gint32 load_image (char   *filename);
static gint   save_image (char   *filename,
			  gint32  image_ID,
			  gint32  drawable_ID);

GPlugInInfo PLUG_IN_INFO =
{
  NULL,    /* init_proc */
  NULL,    /* quit_proc */
  query,   /* query_proc */
  run,     /* run_proc */
};

#ifdef __cplusplus
} /* extern "C" */
#endif

MAIN()

static void
query ()
{
  static GParamDef load_args[] =
  {
    { PARAM_INT32, "run_mode", "Interactive, non-interactive" },
    { PARAM_STRING, "filename", "The name of the file to load" },
    { PARAM_STRING, "raw_filename", "The name of the file to load" },
  };
  static GParamDef load_return_vals[] =
  {
    { PARAM_IMAGE, "image", "Output image" },
  };
  static int nload_args = sizeof (load_args) / sizeof (load_args[0]);
  static int nload_return_vals = sizeof (load_return_vals) / sizeof (load_return_vals[0]);

  static GParamDef save_args[] =
  {
    { PARAM_INT32, "run_mode", "Interactive, non-interactive" },
    { PARAM_IMAGE, "image", "Input image" },
    { PARAM_DRAWABLE, "drawable", "Drawable to save" },
    { PARAM_STRING, "filename", "The name of the file to save the image in" },
    { PARAM_STRING, "raw_filename", "The name of the file to save the image in" }
  };
  static int nsave_args = sizeof (save_args) / sizeof (save_args[0]);

  gimp_install_procedure ("file_openexr_load",
                          "Loads files in the OpenEXR file format",
			  "Loads OpenEXR image files.",
                          "Drew Hess <dhess@ilm.com>",
                          "Copyright 2002 Lucas Digital Ltd.",
                          "0.0.1 - Oct 10 2002",
                          "<Load>/OpenEXR",
                          NULL,
                          PROC_PLUG_IN,
                          nload_args, nload_return_vals,
                          load_args, load_return_vals);

  gimp_install_procedure ("file_openexr_save",
                          "Saves files in the OpeneXR file format",
                          "Saves OpenEXR image files.",
                          "Drew Hess <dhess@ilm.com>",
                          "Copyright 2002 Lucas Digital Ltd.",
                          "0.0.1 - Oct 10 2002",
                          "<Save>/OpenEXR",
                          "FLOAT_RGB",
                          PROC_PLUG_IN,
                          nsave_args, 0,
                          save_args, NULL);

  gimp_register_magic_load_handler("file_openexr_load", "exr", "", "");
  gimp_register_save_handler ("file_openexr_save", "exr", "");
}

static void
run (char    *name,
     int      nparams,
     GParam  *param,
     int     *nreturn_vals,
     GParam **return_vals)
{
  static GParam values[2];
  gint32 image_ID;

  *nreturn_vals = 1;
  *return_vals = values;
  values[0].type = PARAM_STATUS;
  values[0].data.d_status = STATUS_CALLING_ERROR;

  if (strcmp (name, "file_openexr_load") == 0)
    {
      image_ID = load_image (param[1].data.d_string);

      if (image_ID != -1)
	{
	  *nreturn_vals = 2;
	  values[0].data.d_status = STATUS_SUCCESS;
	  values[1].type = PARAM_IMAGE;
	  values[1].data.d_image = image_ID;
	}
      else
	{
	  values[0].data.d_status = STATUS_EXECUTION_ERROR;
	}
    }
  else if (strcmp (name, "file_openexr_save") == 0)
    {
      *nreturn_vals = 1;
      if (save_image (param[3].data.d_string, param[1].data.d_int32, param[2].data.d_int32))
	  values[0].data.d_status = STATUS_SUCCESS;
      else
	  values[0].data.d_status = STATUS_EXECUTION_ERROR;
    }
}

static gint32
load_image (char *filename)
{
    half * scanline = 0;
    gfloat ** pixels = 0;

    try
    {
	Imf::RgbaInputFile exr_full (filename);
	Imf::InputFile exr (filename);

	// Set up progress meter.

	char progress[PATH_MAX];
	if (strrchr (filename, '/') != NULL)
	    snprintf (progress, PATH_MAX, "Loading %s",
		      strrchr (filename, '/') + 1);
	else
	    snprintf (progress, PATH_MAX, "Loading %s", filename);
	gimp_progress_init(progress);

	// Get image dimensions and set up gimp image/layer variables
	
	const Imath::Box2i & dataWindow = exr_full.dataWindow ();

	int width = dataWindow.max.x - dataWindow.min.x + 1;
	int height = dataWindow.max.y - dataWindow.min.y + 1;
	GImageType image_type = FLOAT_RGB;
	GDrawableType layer_type = FLOAT_RGBA_IMAGE;
	int nchan=4;
	if (exr_full.channels() == Imf::WRITE_RGB){
	  layer_type=FLOAT_RGB_IMAGE;
	  nchan=3;
	  fprintf(stderr, "3 channel image\n");
	}
	
	GImageType image_ID = (GImageType) gimp_image_new (width, height, 
							   image_type);
	gimp_image_set_filename (image_ID, filename);
	GDrawableType layer_ID = (GDrawableType) gimp_layer_new(image_ID, 
								"Background", 
 							        width, height,
							        layer_type, 100, NORMAL_MODE);
	gimp_image_add_layer(image_ID, layer_ID, 0);
	GDrawable * drawable = gimp_drawable_get (layer_ID);

	GPixelRgn pixel_rgn;
	gimp_pixel_rgn_init (&pixel_rgn, drawable, 0, 0, drawable->width, 
			     drawable->height, TRUE, FALSE);

	int tile_height = gimp_tile_height();

	// Set up temporary buffers

	scanline = new half [width*nchan];
	Imf::FrameBuffer fb;
	size_t xstride = sizeof (half) * nchan;
	fb.insert ("R", Imf::Slice (Imf::HALF, (char *) scanline, xstride, 0));
	fb.insert ("G", Imf::Slice (Imf::HALF, 
				    (char *) scanline + 1 * sizeof (half), 
				    xstride, 0));
	fb.insert ("B", Imf::Slice (Imf::HALF,
				    (char *) scanline + 2 * sizeof (half), 
				    xstride, 0));
	if (nchan == 4)
	    fb.insert ("A", Imf::Slice (Imf::HALF,
					(char *) scanline + 3 * sizeof (half), 
					xstride, 0));

	pixels = g_new(gfloat*, tile_height);
	if (!pixels)
	    THROW (Iex::BaseExc, "Out of memory");

	pixels[0] = g_new(gfloat, tile_height * width * nchan);
	if (!pixels[0])
	    THROW (Iex::BaseExc, "Out of memory");

	for (int i = 1; i < tile_height; i++) {
	    pixels[i] = pixels[i - 1] + width * nchan;
	}

	exr.setFrameBuffer (fb);

	int y = 0;
	while (y < height)
	{
	    int start = y;
	    int end = MIN (y + tile_height, height);
	    int scanlines = end - start;
	    for (int i = 0; i != scanlines; ++i)
	    {
		exr.readPixels (y);
		for (int x = 0; x != width; ++x)
		{
		    gfloat * floatPixel = pixels[i] + x * nchan;
		    half * half = scanline + x*nchan;
		    *floatPixel++ = (gfloat) half[0];
		    *floatPixel++ = (gfloat) half[1];
		    *floatPixel++ = (gfloat) half[2];
		     if (nchan == 4){
		       *floatPixel++ = (gfloat) half[3];
		     }
		}
		y++;
	    }
	    gimp_pixel_rgn_set_rect(&pixel_rgn, (guchar *)pixels[0], 0, start, 
				    drawable->width, scanlines);
	    gimp_progress_update((double)y / (double)height);
	}

        // Clean up

        g_free(pixels[0]);
        g_free(pixels);
        delete [] scanline;

        // Update the display
        gimp_drawable_flush(drawable);
        gimp_drawable_detach(drawable);
        return image_ID;
    }
    catch (Iex::BaseExc & e)
    {
	delete [] scanline;
	if (pixels && pixels[0])
	    g_free (pixels[0]);
	if (pixels)
	    g_free (pixels);

	g_print (e.what ());
	gimp_quit ();
    }
}

static gint
save_image (char   *filename,
	    gint32  image_ID,
	    gint32  drawable_ID)
{
    GDrawableType drawable_type = gimp_drawable_type(drawable_ID);
    GDrawable * drawable = gimp_drawable_get(drawable_ID);

    int width = drawable->width;
    int height = drawable->height;
    
    int nchan;
    switch (drawable_type) {
      case FLOAT_RGBA_IMAGE:
	  nchan = 4;
	  break;
      case FLOAT_RGB_IMAGE:
	/*   nchan = 3; */
	  g_message ("Please add an alpha channel before saving as OpenEXR\n");
	  return FALSE;
	  break;
	  
	  // support other types later

      case GRAY_IMAGE:
      case GRAYA_IMAGE:
      case U16_GRAY_IMAGE:
      case U16_GRAYA_IMAGE:
      case FLOAT16_GRAY_IMAGE:
      case FLOAT16_GRAYA_IMAGE:
      case FLOAT_GRAY_IMAGE:
      case FLOAT_GRAYA_IMAGE:
	  g_message ("Converting grayscale images to OpenEXR is currently unsupported\n");
	  return FALSE;
	  break;
	  
      default:
	g_message ("Please convert the image to float before saving as OpenEXR\n");
	return FALSE;
	break;
    }

    fprintf(stderr, "Writing %d channels to %s\n", nchan, filename);
    
    half * scanline = 0;
    gfloat ** pixels = 0;
    try
    {
	// write an RGBA OpenEXR file with default settings and type HALF
	// pixels.

	Imf::RgbaOutputFile exr (filename, width, height,
				 nchan == 4 ? Imf::WRITE_RGBA :
				 Imf::WRITE_RGB);

	// Write a scanline at a time to save memory.  We do this by
	// setting the frame buffer y stride to 0.

	scanline = new half [width * nchan];
	Imf::FrameBuffer fb;
	size_t xstride = sizeof (half) * nchan;
	fb.insert ("R", Imf::Slice (Imf::HALF, (char *) scanline, xstride, 0));
	fb.insert ("G", Imf::Slice (Imf::HALF, 
				    (char *) scanline + 1 * sizeof (half), 
				    xstride, 0));
	fb.insert ("B", Imf::Slice (Imf::HALF,
				    (char *) scanline + 2 * sizeof (half), 
				    xstride, 0));
	if (nchan == 4)
	    fb.insert ("A", Imf::Slice (Imf::HALF,
					(char *) scanline + 3 * sizeof (half), 
					xstride, 0));
	
	exr.setFrameBuffer ((const Imf::Rgba *) scanline, 1, 0);

	char progress[PATH_MAX];
	if (strrchr (filename, '/') != NULL)
	    snprintf (progress, PATH_MAX, "Saving %s",
		      strrchr (filename, '/') + 1);
	else
	    snprintf (progress, PATH_MAX, "Saving %s", filename);
	gimp_progress_init(progress);

	// Set up temp buffers

	int tile_height = gimp_tile_height();
	pixels = g_new(gfloat*, tile_height);
	pixels[0] = g_new(gfloat, tile_height * width * nchan);
	for (int i = 1; i < tile_height; i++) {
	    pixels[i] = pixels[i - 1] + width * nchan;
	}

	// Save the image
	
	GPixelRgn pixel_rgn;
	gimp_pixel_rgn_init(&pixel_rgn, drawable, 0, 0, 
			    width, height, FALSE, FALSE);
	int y = 0;
	while (y < height) {
	    int start = y;
	    int end = MIN(y + tile_height, height);
	    int scanlines = end - start;
	    gimp_pixel_rgn_get_rect(&pixel_rgn, (guchar *) pixels[0], 
				    0, start, width, scanlines);

	    for (int i = 0; i < scanlines; i++) {
		if (nchan == 4)
		{
		    for (int x = 0; x < width; x++) {
			float * floatPixel = pixels[i] + x * 4;
			half * halfPixel = scanline + x * 4;
			*halfPixel++ = floatPixel[0];
			*halfPixel++ = floatPixel[1];
			*halfPixel++ = floatPixel[2];
			*halfPixel++ = floatPixel[3];
		    }
		}
		else
		{
		    for (int x = 0; x < width; x++) {
			float * floatPixel = pixels[i] + x * 3;
			half * halfPixel = scanline + x * 3;
			*halfPixel++ = floatPixel[0];
			*halfPixel++ = floatPixel[1];
			*halfPixel++ = floatPixel[2];
		    }
		}
		exr.writePixels (1);
		y++;
	    }
	    gimp_progress_update((double)y / (double)height);
	}

	// clean up

	g_free (pixels[0]);
	g_free (pixels);
	delete [] scanline;

	return TRUE;
    }
    catch (Iex::BaseExc & e)
    {
	delete [] scanline;
	if (pixels && pixels[0])
	    g_free (pixels[0]);
	if (pixels)
	    g_free (pixels);

	g_message (e.what ());
	return FALSE;
    }
}
