/* Tiny display-an-image demo program. 
 *
 * This is not supposed to be a complete image viewer, it's just supposed to 
 * show how to display a VIPS image (or the result of a VIPS computation) in a
 * window.
 *
 * 8-bit RGB images only, though it would be easy to fix this.
 *
 * Compile with:

	g++ -g -Wall `pkg-config vipsCC-7.22 gtkmm-2.4 --cflags --libs` \
		gtkdisp.cc  -o gtkdisp

 */

#include <stdio.h>
#include <gtkmm.h>
#include <vips/vips>

using namespace vips;

/* We need the C API as well for the format stuff, and to create regions.
 */
#include <vips/vips.h>

/* We decompress some images to a temp file for display. vips-7.24 has a much
 * better mechanism for handling this, but for now just use this file.
 */
#define TEMP_FILE "/tmp/tmp.v"

/* Just to demo progress feedback. This should be used to update a widget
 * somewhere.
 */
static int
load_progress (IMAGE * image)
{
  static int last_percent = 0;

  if (image->time->percent - last_percent >= 10)
    {
      printf ("loading %d%% complete ...\n", image->time->percent);
      last_percent = image->time->percent;
    }

  return (0);
}

/* vips-7.24 has a new "rd" mode that does more-or-less this for you, but for
 * 7.22 we need to decompress to a file ourselves.
 */
static VImage
load_image (const char *filename)
{
  VipsFormatClass *format;
  IMAGE *image;

  if (!(format = vips_format_for_file (filename)))
    verror ();

  /* Can we open the image directly, or should we decompress to a temp
   * file and display from there?
   */
  if (format->get_flags && 
      format->get_flags (filename) & VIPS_FORMAT_PARTIAL)
    {
      if (!(image = im_open (filename, "r")))
	verror ();
    }
  else
    {
      if (!(image = im_open (TEMP_FILE, "w")))
	verror ();
      im_add_eval_callback (image, 
		            (im_callback_fn) load_progress, image, NULL);
      im_add_postclose_callback (image,
				 (im_callback_fn) unlink, (void *) TEMP_FILE,
				 NULL);
      if (format->load (filename, image))
	{
	  im_close (image);
	  verror ();
	}
    }

  /* Wrap up the IMAGE* as a VImage.
   */
  VImage im (image);

  return (im);
}

/* Subclass DrawingArea to make a widget that displays a VImage.
 */
class VImageArea : public Gtk::DrawingArea
{
private:
  /* The image we display.
   */
  VImage image;

  /* The derived image we paint to the screen.
   */
  VImage display_image;

  /* The region we prepare from to draw the pixels,
   */
  REGION * region;

  /* We send this packet of data from the bg worker thread to the main GUI
   * thread whena tile has been calculated.
   */
  typedef struct {
    VImageArea * image_area;
    Rect rect;
  } Update;

  /* The main GUI thread runs this when it's idle and there are tiles that need
   * painting. 
   */
  static gboolean render_cb (Update * update)
  {
    update->image_area->queue_draw_area (update->rect.left, 
		                         update->rect.top,
					 update->rect.width,
					 update->rect.height);

    g_free (update);

    return (FALSE);
  }

  /* Come here from the im_render() background thread when a tile has been
   * calculated. We can't paint the screen directly since the main GUI thread
   * might be doing something. Instead, we add an idle callback which will be
   * run by the main GUI thread when it next hits the mainloop.
   */
  static void
  render_notify (IMAGE *image, Rect *rect, void *client)
  {
    VImageArea * image_area = (VImageArea *) client;
    Update * update = g_new (Update, 1);

    update->rect = *rect;
    update->image_area = image_area;

    g_idle_add ((GSourceFunc) render_cb, update);
  }

  /* Edit these to add or remove things from the display pipe we build.
   * These should be wired up to something in a GUI.
   */
  static const gboolean zoom_in = FALSE;
  static const gboolean zoom_out = FALSE;
  static const gboolean edge_detect = FALSE;

  /* Make the image for display from the raw disc image. Could do
   * anything here, really. Uncomment sections to try different effects. 
   * Convert to 8-bit RGB would be a good idea.
   */
  void
  build_display_image ()
  {
    VImage t = image;

    if (zoom_out)
      t = t.subsample (4, 4);

    if (zoom_in)
      t = t.zoom (4, 4);

    if (edge_detect)
      {
	VIMask m (3, 3, 1, 0, -1, -1, -1, -1, 8, -1, -1, -1, -1);

	t = t.conv (m);
      }

    /* vips_sink_screen() is not wrapped by C++ ... we need to drop down to C
     * here.
     *
     * We ask for a cache of 1000 64x64 tiles, enough to be able to repaint a
     * 1920 x 1200 screen, plus a bit.
     */
    if (vips_sink_screen (t.image (), display_image.image (), NULL,
			  64, 64, 1000, 0, render_notify, this))
      verror ();

    /* display_image depends on t .. we need to keep t alive as long
     * as display_image is alive.
     */
    display_image._ref->addref (t._ref);
  }

  void
  expose_rect (GdkRectangle * expose)
  {
    /* Clip against the image size ... we don't want to try painting outside the
     * image area.
     */
    Rect image = {0, 0, display_image.Xsize (), display_image.Ysize ()};
    Rect area = {expose->x, expose->y, expose->width, expose->height};
    Rect clip;

    im_rect_intersectrect (&image, &area, &clip);
    if (im_rect_isempty (&clip))
      return;

    /* Make sure this im_prepare() will really do a calculation. We don't want
     * the cached result from the previous call: it might have been from before
     * the background thread made the pixels we want.
     */
    im_invalidate (region->im);

    /* Calculate pixels. If this area is not in cache, we will see black
     * pixels, a background thread will start calculating stuff, and we will
     * get a notify callback from the bg thread when our pixels area ready. If
     * the area is in cache, we see pixels immediately.
     *
     * If we took the trouble, we could use the mask image to see what parts
     * of the resulting region were from cache and what parts were
     * uncalculated.
     */
    if (im_prepare (region, &clip))
      return;
    guchar *buf = (guchar *) IM_REGION_ADDR (region, clip.left, clip.top);
    int lsk = IM_REGION_LSKIP (region);

    get_window ()->draw_rgb_image (get_style ()->get_white_gc (),
				   clip.left, clip.top, clip.width, clip.height,
				   Gdk::RGB_DITHER_MAX, buf, lsk);
  }

  virtual bool on_expose_event (GdkEventExpose * event)
  {
    GdkRectangle *expose;
    int i, n;

    gdk_region_get_rectangles (event->region, &expose, &n);
    for (i = 0; i < n; i++)
      expose_rect (&expose[i]);
    g_free (expose);

    return (TRUE);
  }

public:
  VImageArea ()
  {
    region = NULL;
  }

  virtual ~ VImageArea ()
  {
    if (region)
      {
	im_region_free (region);
        region = NULL;
      }
  }

  void
  set_image (VImage new_image)
  {
    image = new_image;

    /* Reset the display image.
     */
    VImage null;
    display_image = null;
    if (region)
      {
	im_region_free (region);
	region = NULL;
      }

    build_display_image ();
    region = im_region_create (display_image.image ());
    set_size_request (display_image.Xsize (), display_image.Ysize ());
  }
};

int
main (int argc, char **argv)
{
  Gtk::Main kit (argc, argv);

  if (argc != 2)
    error_exit ("usage: %s <filename>", argv[0]);
  if (im_init_world (argv[0]))
    verror ();

  VImage image = load_image (argv[1]);

  Gtk::Window window;
  window.set_default_size (500, 500);
  Gtk::ScrolledWindow scrolled_window;
  window.add (scrolled_window);
  VImageArea drawing_area;
  drawing_area.set_image (image);
  scrolled_window.add (drawing_area);
  window.show_all ();

  Gtk::Main::run (window);

  return 0;
}
