#include "w3mimg/w3mimg.h"

#if defined(USE_IMLIB)
#include <Imlib.h>
#elif defined(USE_GDKPIXBUF)
#include <gdk-pixbuf/gdk-pixbuf-xlib.h>
#else
#error neither Imlib nor GdkPixbuf support
#endif

struct x11_w3mimg_desc {
  Display *display;
  Window window, parent;
  unsigned long background_pixel;
  Region screenRegion, clipRegion, exposureRegion, imageRegion, curRegion;
  GC imageGC;
#if defined(USE_IMLIB)
  ImlibData *id;
#elif defined(USE_GDKPIXBUF)
  int init_flag;
#endif
  XPoint screenRectangle[5];
  int prev_xexpose_count, nsyncs, nevents;
  int ignore_xexpose;
};

#if defined(USE_GDKPIXBUF)
struct x11_w3mimg_pixmaps {
  int total;
  int no;
  int wait;
  Pixmap *pixmap;
};
#endif

static void
x11_img_init(w3mimg_desc *desc)
{
  struct x11_w3mimg_desc *xdesc;

#if defined(USE_IMLIB)
  if ((xdesc = desc->priv) &&
      !xdesc->id && !(xdesc->id = Imlib_init(xdesc->display))) {
    XCloseDisplay(xdesc->display);
    memset(xdesc, 0, sizeof(*xdesc));
    desc->priv = NULL;
  }
#elif defined(USE_GDKPIXBUF)
  if ((xdesc = desc->priv) && !xdesc->init_flag) {
    gdk_pixbuf_xlib_init(xdesc->display, 0);
    xdesc->init_flag = TRUE;
  }
#endif
}

static void
x11_img_free(w3mimg_desc *desc, W3MImage *image)
{
  struct x11_w3mimg_desc *xdesc;
#ifndef USE_GDKPIXBUF
  Pixmap map;

  if ((xdesc = desc->priv) && (map = (Pixmap)image->pixmap)) {
    XFreePixmap(xdesc->display, map);
    image->pixmap = NULL;
  }
#else
  struct x11_w3mimg_pixmaps *map;

  if ((xdesc = desc->priv) && (map = image->pixmap)) {
    if (map->pixmap) {
      int i;

      for (i = map->total ; i > 0 ;)
	if (map->pixmap[--i])
	  XFreePixmap(xdesc->display, map->pixmap[i]);

      free(map->pixmap);
    }

    free(map);
    image->pixmap = NULL;
  }
#endif
}

#if defined(USE_GDKPIXBUF)
static struct x11_w3mimg_pixmaps *
x11_img_new_pixmap(struct x11_w3mimg_desc *xi, int w, int h, int n)
{
  struct x11_w3mimg_pixmaps *img = NULL;
  int i;

  if (!(img = malloc(sizeof(*img))))
    goto error;

  if (!(img->pixmap = calloc(n, sizeof(*(img->pixmap)))))
    goto error;

  XSetClipMask(xi->display, xi->imageGC, None);
  XSetForeground(xi->display, xi->imageGC, xi->background_pixel);

  for (i = n ; i > 0 ;) {
    if (!(img->pixmap[--i] = XCreatePixmap(xi->display, xi->parent, w, h,
					   DefaultDepth(xi->display, 0))))
      goto error;

    XFillRectangle(xi->display, img->pixmap[i], xi->imageGC, 0, 0, w, h);
  }

  img->no = 0;
  img->total = n;
  img->wait = 0;
  return img;
error:
  if (img) {
    if (img->pixmap) {
      for (i = n ; i > 0 ;)
	if (img->pixmap[--i])
	  XFreePixmap(xi->display, img->pixmap[i]);

      free(img->pixmap);
    }

    free(img);
  }

  return NULL;
}

static GdkPixbuf *
x11_img_resize_pixbuf(GdkPixbuf *pixbuf, int width, int height)
{
  int w, h;

  if (!pixbuf || width < 1 || height < 1)
    return pixbuf;

  w = gdk_pixbuf_get_width(pixbuf);
  h = gdk_pixbuf_get_height(pixbuf);

  if (w == width && h == height)
    return pixbuf;

  return gdk_pixbuf_scale_simple(pixbuf, width, height, GDK_INTERP_BILINEAR);
}
#endif

static void
x11_img_load(w3mimg_desc *desc, W3MImage *image, char *p, int w, int h)
{
  struct x11_w3mimg_desc *xdesc;

  if ((xdesc = desc->priv)) {
#if defined(USE_IMLIB)
    ImlibImage *im;

    if ((im = Imlib_load_image(xdesc->id, p))) {
      if (w <= 0)
	w = im->rgb_width;

      if (h <= 0)
	h = im->rgb_height;

      if (!(image->pixmap = (void *)XCreatePixmap(xdesc->display, xdesc->parent, w, h,
						  DefaultDepth(xdesc->display, 0)))) {
	Imlib_kill_image(xdesc->id, im);
	return;
      }

      if (!xdesc->imageGC &&
	  !(xdesc->imageGC = XCreateGC(xdesc->display, xdesc->parent, 0, NULL))) {
	Imlib_kill_image(xdesc->id, im);
	return;
      }

      XSetClipMask(xdesc->display, xdesc->imageGC, None);
      XSetForeground(xdesc->display, xdesc->imageGC, xdesc->background_pixel);
      XFillRectangle(xdesc->display, (Pixmap)image->pixmap, xdesc->imageGC, 0, 0, w, h);
      Imlib_paste_image(xdesc->id, im, (Pixmap)image->pixmap, 0, 0, w, h);
      Imlib_kill_image(xdesc->id, im);
      image->width = w;
      image->height = h;
    }
#elif defined(USE_GDKPIXBUF)
    GdkPixbufAnimation *animation;

    if ((animation = gdk_pixbuf_animation_new_from_file(p))) {
      GList *frames;
      int i, iw, ih, n;
      double ratio_w, ratio_h;
      struct x11_w3mimg_pixmaps *ximg;
      GdkPixbufFrameAction action = GDK_PIXBUF_FRAME_REVERT;

      frames = gdk_pixbuf_animation_get_frames(animation);
      n = gdk_pixbuf_animation_get_num_frames(animation);
      iw = gdk_pixbuf_animation_get_width(animation);
      ih = gdk_pixbuf_animation_get_height(animation);

      if (w < 1 || h < 1) {
	w = iw;
	h = ih;
	ratio_w = ratio_h = 1;
      }
      else {
	ratio_w = 1.0 * w / iw;
	ratio_h = 1.0 * h / ih;
      }

      if (!xdesc->imageGC &&
	  !(xdesc->imageGC = XCreateGC(xdesc->display, xdesc->parent, 0, NULL))) {
	gdk_pixbuf_animation_unref(animation);
	return;
      }

      if (!(ximg = x11_img_new_pixmap(xdesc, w, h, n))) {
	gdk_pixbuf_animation_unref(animation);
	return;
      }

      for (i = 0 ; i < n ; ++i) {
	GdkPixbufFrame *frame;
	GdkPixbuf *org_pixbuf, *pixbuf;
	int width, height, ofstx, ofsty;

	frame = (GdkPixbufFrame *)g_list_nth_data(frames, i);
	org_pixbuf = gdk_pixbuf_frame_get_pixbuf(frame);
	ofstx = gdk_pixbuf_frame_get_x_offset(frame);
	ofsty = gdk_pixbuf_frame_get_y_offset(frame);
	width = gdk_pixbuf_get_width(org_pixbuf);
	height = gdk_pixbuf_get_height(org_pixbuf);

	if (!ofstx && !ofsty && width == w && height == h)
	  pixbuf = x11_img_resize_pixbuf(org_pixbuf, w, h);
	else {
	  pixbuf = x11_img_resize_pixbuf(org_pixbuf, width * ratio_w, height * ratio_h);
	  ofstx *= ratio_w;
	  ofsty *= ratio_h;
	}

	width = gdk_pixbuf_get_width(pixbuf);
	height = gdk_pixbuf_get_height(pixbuf);

	if (i > 0)
	  switch (action) {
	  case GDK_PIXBUF_FRAME_RETAIN:
	    XCopyArea(xdesc->display, ximg->pixmap[i - 1], ximg->pixmap[i],
		      xdesc->imageGC, 0, 0, w, h, 0, 0);
	    break;
	  case GDK_PIXBUF_FRAME_DISPOSE:
	    break;
	  case GDK_PIXBUF_FRAME_REVERT:
	    XCopyArea(xdesc->display, ximg->pixmap[0], ximg->pixmap[i],
		      xdesc->imageGC, 0, 0, w, h, 0, 0);
	    break;
	  default:
	    XCopyArea(xdesc->display, ximg->pixmap[0], ximg->pixmap[i],
		      xdesc->imageGC, 0, 0, w, h, 0, 0);
	    break;
	  }

	gdk_pixbuf_xlib_render_to_drawable_alpha(pixbuf,
						 (Drawable)ximg->pixmap[i], 0,
						 0, ofstx, ofsty, width,
						 height,
						 GDK_PIXBUF_ALPHA_BILEVEL, 1,
						 XLIB_RGB_DITHER_NORMAL, 0, 0);
	action = gdk_pixbuf_frame_get_action(frame);

	if (org_pixbuf != pixbuf)
	  gdk_pixbuf_finalize(pixbuf);
      }

      gdk_pixbuf_animation_unref(animation);
      image->pixmap = ximg;
      image->width = w;
      image->height = h;
    }
#endif
  }
}

static void
x11_img_clip(w3mimg_desc *desc, char *p)
{
  struct x11_w3mimg_desc *xdesc;

  if ((xdesc = desc->priv)) {
    char *ep;
    int i;
    short val[4];
    XRectangle rect;

    if (xdesc->exposureRegion && !XEmptyRegion(xdesc->exposureRegion))
      XSubtractRegion(xdesc->exposureRegion, xdesc->exposureRegion, xdesc->exposureRegion);

    if (xdesc->clipRegion)
      XSubtractRegion(xdesc->clipRegion, xdesc->clipRegion, xdesc->clipRegion);
    else if (!(xdesc->clipRegion = XCreateRegion()))
      w3mimg_exit(1, stderr, "clipRegion = XCreateRegion(): %s\n", strerror(errno));

    for (i = 0 ; *p ;) {
      val[i++] = strtol(p, &ep, 10);

      if (i == sizeof(val) / sizeof(val[0])) {
	rect.x = val[0] + desc->offset_x;
	rect.y = val[1] + desc->offset_y;
	rect.width = val[2];
	rect.height = val[3];
	XUnionRectWithRegion(&rect, xdesc->clipRegion, xdesc->clipRegion);
	i = 0;
      }

      p = *ep ? ep + 1 : ep;
    }
  }
}

static void
x11_clear(w3mimg_desc *desc)
{
  struct x11_w3mimg_desc *xdesc;

  if ((xdesc = desc->priv)) {
    if (xdesc->clipRegion) {
      XDestroyRegion(xdesc->clipRegion);
      xdesc->clipRegion = NULL;
    }

    if (xdesc->exposureRegion) {
      XDestroyRegion(xdesc->exposureRegion);
      xdesc->exposureRegion = NULL;
    }

    if (xdesc->screenRegion) {
      XDestroyRegion(xdesc->screenRegion);
      xdesc->screenRegion = NULL;
    }

    if (xdesc->imageRegion) {
      XDestroyRegion(xdesc->imageRegion);
      xdesc->imageRegion = NULL;
    }

    xdesc->curRegion = NULL;

    if (xdesc->imageGC) {
      XFreeGC(xdesc->display, xdesc->imageGC);
      xdesc->imageGC = NULL;
    }
  }
}

static int
x11_img_flush_start(w3mimg_desc *desc)
{
  struct x11_w3mimg_desc *xdesc;

  if (!(xdesc = desc->priv) ||
      XEmptyRegion(xdesc->curRegion) ||
      (!xdesc->imageGC &&
       !(xdesc->imageGC = XCreateGC(xdesc->display, xdesc->parent, 0, NULL))))
    return 0;

  if ((desc->clipped = xdesc->clipRegion && !XEmptyRegion(xdesc->clipRegion))) {
    if (xdesc->imageRegion) {
      if (!XEmptyRegion(xdesc->imageRegion))
	XSubtractRegion(xdesc->imageRegion, xdesc->imageRegion, xdesc->imageRegion);
    }
    else if (!(xdesc->imageRegion = XCreateRegion()))
      return 0;

    XSubtractRegion(xdesc->curRegion, xdesc->clipRegion, xdesc->imageRegion);
    XSetRegion(xdesc->display, xdesc->imageGC, xdesc->imageRegion);
  }
  else if (xdesc->curRegion == xdesc->screenRegion)
    XSetClipMask(xdesc->display, xdesc->imageGC, None);
  else
    XSetRegion(xdesc->display, xdesc->imageGC, xdesc->curRegion);

  desc->alldirty = xdesc->curRegion == xdesc->exposureRegion;
  return 1;
}

static void
x11_img_show(w3mimg_desc *desc, W3MImage *image,
	     int sx, int sy, int sw, int sh, int x, int y)
{
  struct x11_w3mimg_desc *xdesc;

  if ((xdesc = desc->priv)) {
#if defined(USE_IMLIB)
    XCopyArea(xdesc->display, (Pixmap)image->pixmap, xdesc->window, xdesc->imageGC,
	      sx, sy, sw, sh, x, y);
#elif defined(USE_GDKPIXBUF)
#define WAIT_CNT 4
    struct x11_w3mimg_pixmaps *ximg;
    int i;

    ximg = image->pixmap;
    i = ximg->no;
    XCopyArea(xdesc->display, ximg->pixmap[i], xdesc->window, xdesc->imageGC,
	      sx, sy,
	      (sw ? sw : image->width),
	      (sh ? sh : image->height), x + desc->offset_x, y + desc->offset_y);

    if (ximg->total > 1) {
      if (ximg->wait > WAIT_CNT) {
	ximg->wait = 0;

	if (i < ximg->total - 1)
	  ximg->no = i + 1;
	else
	  ximg->no = 0;
      }

      ximg->wait += 1;
    }
#endif

    desc->flush_dirty = 1;
  }
}

static int
x11_img_clipped(w3mimg_desc *desc, W3MImage *image, int x, int y, int w, int h)
{
  struct x11_w3mimg_desc *xdesc;

  if ((xdesc = desc->priv))
    switch (XRectInRegion(xdesc->clipRegion, x, y, w, h)) {
    case RectangleIn:
    case RectanglePart:
      return 1;
    default:
      break;
    }

  return 0;
}

static void
x11_serv_write(w3mimg_desc *desc)
{
  struct x11_w3mimg_desc *xdesc;

  if ((xdesc = desc->priv)) {
    XFlush(xdesc->display);
    desc->flush_dirty = 0;
  }
}

static void
x11_serv_read(w3mimg_desc *desc)
{
  struct x11_w3mimg_desc *xdesc;

  if ((xdesc = desc->priv) && XEventsQueued(xdesc->display, QueuedAfterReading)) {
    XEvent event;

    do {
      XNextEvent(xdesc->display, &event);

      if (event.type == Expose && event.xexpose.window == xdesc->window) {
	XRectangle rect;

	if (!xdesc->prev_xexpose_count && xdesc->exposureRegion && !XEmptyRegion(xdesc->exposureRegion))
	  XSubtractRegion(xdesc->exposureRegion, xdesc->exposureRegion, xdesc->exposureRegion);

	if (!xdesc->exposureRegion &&
	    !(xdesc->exposureRegion = XCreateRegion()))
	  w3mimg_exit(1, stderr, "xdesc->exposureRegion = XCreateRegion(): %s\n", strerror(errno));

	rect.x = event.xexpose.x;
	rect.y = event.xexpose.y;
	rect.width = event.xexpose.width;
	rect.height = event.xexpose.height;
	XUnionRectWithRegion(&rect, xdesc->exposureRegion, xdesc->exposureRegion);

	if (!(xdesc->prev_xexpose_count = event.xexpose.count))
	  xdesc->nevents += desc->flush_delay;
      }
    } while (XEventsQueued(xdesc->display, QueuedAlready));
  }
}

static void
x11_serv_sync(w3mimg_desc *desc)
{
  struct x11_w3mimg_desc *xdesc;

  if ((xdesc = desc->priv))
    xdesc->nsyncs += desc->flush_delay;
}

static void
x11_serv_idle(w3mimg_desc *desc)
{
  struct x11_w3mimg_desc *xdesc;

  if ((xdesc = desc->priv)) {
    if (xdesc->nsyncs) {
      if (!--(xdesc->nsyncs)) {
	if (xdesc->exposureRegion && !XEmptyRegion(xdesc->exposureRegion))
	  XSubtractRegion(xdesc->exposureRegion, xdesc->exposureRegion, xdesc->exposureRegion);

	if (!xdesc->screenRegion &&
	    !(xdesc->screenRegion = XPolygonRegion(xdesc->screenRectangle,
						   sizeof(xdesc->screenRectangle) / sizeof(xdesc->screenRectangle[0]),
						   WindingRule)))
	  w3mimg_exit(1, stderr, "xdesc->screenRegion = XPolygonRegion(): %s\n", strerror(errno));

	xdesc->curRegion = xdesc->screenRegion;
	w3mimg_flush(desc);
	xdesc->nevents = 0;
	xdesc->prev_xexpose_count = -1;
      }
    }
    else if (xdesc->nevents) {
      if (!--(xdesc->nevents) && !xdesc->ignore_xexpose && xdesc->exposureRegion && !XEmptyRegion(xdesc->exposureRegion)) {
	xdesc->curRegion = xdesc->exposureRegion;
	w3mimg_flush(desc);
      }
    }
  }
}

static void
x11_serv_close(w3mimg_desc *desc)
{
  struct x11_w3mimg_desc *xdesc;

  if ((xdesc = desc->priv)) {
    XCloseDisplay(xdesc->display);
    desc->priv = NULL;
  }
}

/*
  xterm/kterm/hanterm/cxterm
    top window (WINDOWID)
      +- text window
           +- scrollbar
  rxvt/aterm/Eterm/wterm
    top window (WINDOWID)
      +- text window
      +- scrollbar
      +- menubar (etc.)
  gnome-terminal
    top window
      +- text window (WINDOWID)
      +- scrollbar
      +- menubar
  mlterm (-s)
    top window
      +- text window (WINDOWID)
      +- scrollbar
  mlterm
    top window = text window (WINDOWID)

  powershell
    top window
      +- window
      |    +- text window
      |    +- scrollbar
      +- menubar (etc.)
  dtterm
    top window
      +- window
           +- window
           |    +- window
           |         +- text window
           |         +- scrollbar
           +- menubar
  hpterm
    top window
      +- window
           +- text window
           +- scrollbar
           +- (etc.)
*/

w3mimg_desc *
w3mimg_x11open(w3mimg_desc *desc, w3mimg_opts *opts)
{
  static struct x11_w3mimg_desc xdesc;
  char *id;
  int revert, nchildren, i;
  XWindowAttributes attr;
  Window root, *children;
  XColor screen_def, exact_def;

  memset(&xdesc, 0, sizeof(xdesc));
  xdesc.prev_xexpose_count = -1;
  desc->priv = NULL;
  desc->serv_fd = -1;

  if (!(xdesc.display = XOpenDisplay(NULL)))
    return NULL;

  if ((id = getenv("WINDOWID")) && *id)
    xdesc.window = (Window)strtol(id, NULL, 0);
  else
    XGetInputFocus(xdesc.display, &xdesc.window, &revert);

  if (!xdesc.window) {
    XCloseDisplay(xdesc.display);
    return NULL;
  }

  XGetWindowAttributes(xdesc.display, xdesc.window, &attr);
  desc->width = attr.width;
  desc->height = attr.height;
  desc->depth = attr.depth;

  while (1) {
    Window p_window;

    XQueryTree(xdesc.display, xdesc.window, &root, &xdesc.parent, &children, &nchildren);

    if (opts->defined_debug)
      fprintf(stderr,
	      "window=%lx root=%lx parent=%lx nchildren=%d width=%d height=%d\n",
	      (unsigned long)xdesc.window, (unsigned long)root, (unsigned long)xdesc.parent,
	      nchildren, desc->width, desc->height);

    p_window = xdesc.window;

    for (i = 0 ; i < nchildren ; ++i) {
      XGetWindowAttributes(xdesc.display, children[i], &attr);

      if (opts->defined_debug)
	fprintf(stderr,
		"children[%d]=%lx x=%d y=%d width=%d height=%d\n", i,
		(unsigned long)children[i], attr.x, attr.y, attr.width, attr.height);

      if (attr.width > desc->width * 0.7 && attr.height > desc->height * 0.7) {
	/* maybe text window */
	desc->width = attr.width;
	desc->height = attr.height;
	desc->depth = attr.depth;
	xdesc.window = children[i];
      }
    }

    if (p_window == xdesc.window)
      break;
  }

  if (!opts->defined_x) {
    desc->offset_x = desc->offset_y = 2; /* ??? */

    for (i = 0 ; i < nchildren ; ++i) {
      XGetWindowAttributes(xdesc.display, children[i], &attr);

      if (attr.x <= 0 && attr.width < 30 && attr.height > desc->height * 0.7) {
	if (opts->defined_debug)
	  fprintf(stderr,
		  "children[%d]=%lx x=%d y=%d width=%d height=%d\n",
		  i, (unsigned long)children[i], attr.x, attr.y, attr.width, attr.height);

	/* scrollbar of xterm/kterm ? */
	desc->offset_x += attr.x + attr.width + attr.border_width * 2;
	break;
      }
    }
  }

  xdesc.screenRectangle[1].x = xdesc.screenRectangle[2].x = desc->width;
  xdesc.screenRectangle[2].y = xdesc.screenRectangle[3].y = desc->height;

  if (opts->defined_bg && XAllocNamedColor(xdesc.display, DefaultColormap(xdesc.display, 0),
					   opts->background, &screen_def, &exact_def))
    xdesc.background_pixel = screen_def.pixel;
  else {
    XImage *winimg;

    if ((winimg = XGetImage(xdesc.display, xdesc.window,
			    desc->offset_x > 0 ? desc->offset_x - 1 : 0,
			    desc->offset_y > 0 ? desc->offset_y - 1 : 0,
			    1, 1, AllPlanes, XYPixmap))) {
      xdesc.background_pixel = XGetPixel(winimg, 0, 0);
      XDestroyImage(winimg);
    }
    else
      xdesc.background_pixel = WhitePixel(xdesc.display, 0);
  }

  if (!(xdesc.ignore_xexpose = opts->defined_ignore_exposure))
    XSelectInput(xdesc.display, xdesc.window, ExposureMask);

  desc->serv_fd = ConnectionNumber(xdesc.display);
  desc->init_func = x11_img_init;
  desc->free_func = x11_img_free;
  desc->load_func = x11_img_load;
  desc->clip_func = x11_img_clip;
  desc->clip_test = x11_img_clipped;
  desc->clear_func = x11_clear;
  desc->flush_start_hook = x11_img_flush_start;
  desc->show_func = x11_img_show;
  desc->serv_write = x11_serv_write;
  desc->serv_read = x11_serv_read;
  desc->serv_sync = x11_serv_sync;
  desc->serv_idle = x11_serv_idle;
  desc->serv_close = x11_serv_close;
  desc->priv = &xdesc;
  return desc;
}
