/*
   Copyright (C) 2002, 2003, 2004, 2005 Free Software Foundation, Inc.

   Author:  Alexander Malmberg <alexander@malmberg.org>

   This file is part of GNUstep.

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.

   This library 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
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; see the file COPYING.LIB.
   If not, see <http://www.gnu.org/licenses/> or write to the 
   Free Software Foundation, 51 Franklin Street, Fifth Floor, 
   Boston, MA 02110-1301, USA.
*/

#include <config.h>

#include <Foundation/NSUserDefaults.h>

#include "x11/XGServer.h"
#include "x11/XGServerWindow.h"
#include "x11/XWindowBuffer.h"

#include <math.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#ifdef HAVE_XSHAPE
#include <X11/extensions/shape.h>
#endif

static XWindowBuffer **window_buffers;
static int num_window_buffers;


static int use_shape_hack = 0; /* this is an ugly hack : ) */

#ifdef XSHM

static int did_test_xshm = 0;
static int use_xshm = 1;
static Bool use_xshm_pixmaps = 0;
static int num_xshm_test_errors = 0;

static int test_xshm_error_handler(Display *d, XErrorEvent *ev)
{
  num_xshm_test_errors++;
  return 0;
}

static void test_xshm(Display *display, Visual *visual, int drawing_depth)
{
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];

  did_test_xshm = 1;

  if ([ud objectForKey: @"XWindowBufferUseXShm"])
    use_xshm = [ud boolForKey: @"XWindowBufferUseXShm"];

  /* If the user doesn't want XShm, there's no point in doing any further
  testing. */
  if (!use_xshm)
    return;

  /* This seems to return success if the X server understands the XShm
  protocol, regardless of whether XShm can actually be used or not... */
  {
  int major, minor;

  if (!XShmQueryExtension(display) || 
      !XShmQueryVersion(display, &major, &minor, &use_xshm_pixmaps))
    {
      NSLog(@"XShm not supported by X server.");
      NSLog(@"Falling back to normal XImage (will be slower).");
      use_xshm = 0;
      return;
    }
  }

  /* ... so we check that it actually works here. To do this, we need to
  set up our own error handler (because the xlib calls never fail, they
  just cause error events to be sent to us), explicitly synchronize
  (or we wouldn't get to handle, and thus detect, the errors until much
  later), and try actually creating a XShm image. Yuck. */
  {
    XImage *ximage;
    XShmSegmentInfo shminfo;
    int (*old_error_handler)();

    old_error_handler = XSetErrorHandler(test_xshm_error_handler);

    ximage = XShmCreateImage(display, visual,
                             drawing_depth, ZPixmap, NULL, &shminfo,
                             1, 1);
    XSync(display, False);
    if (!ximage || num_xshm_test_errors)
      {
        NSLog(@"XShm not supported, XShmCreateImage failed.");
        goto no_xshm;
      }

    shminfo.shmid = shmget(IPC_PRIVATE,
        64, /* We don't have exact bytes per row values here, but this
            should be safe, and we'll probably get a full page anyway.
             (And if it turns out not to be enough, the X server will notice
            and give us errors, causing us to think that XShm isn't
            supported.) */
        IPC_CREAT | 0700);

    if (shminfo.shmid == -1 || num_xshm_test_errors)
      {
        NSLog(@"XShm not supported, shmget() failed: %m.");
        XDestroyImage(ximage);
        goto no_xshm;
      }

    shminfo.shmaddr = shmat(shminfo.shmid, 0, 0);
    if ((intptr_t)shminfo.shmaddr == -1 || num_xshm_test_errors)
      {
        NSLog(@"XShm not supported, shmat() failed: %m.");
        XDestroyImage(ximage);
        shmctl(shminfo.shmid, IPC_RMID, 0);
        goto no_xshm;
      }

    shminfo.readOnly = 0;
    if (!XShmAttach(display, &shminfo))
       num_xshm_test_errors++;
    XSync(display, False);
    if (num_xshm_test_errors)
      {
        NSLog(@"XShm not supported, XShmAttach() failed.");
        XDestroyImage(ximage);
        shmdt(shminfo.shmaddr);
        shmctl(shminfo.shmid, IPC_RMID, 0);
        goto no_xshm;
      }

    XShmDetach(display, &shminfo);
    XDestroyImage(ximage);
    shmdt(shminfo.shmaddr);

    /* Most of the time, all the above calls will return success despite
    actually failing. To catch all errors generated by the calls before
    returning, we synchronize here. */
    XSync(display, False);

    shmctl(shminfo.shmid, IPC_RMID, 0);

    if (num_xshm_test_errors)
      {
        NSLog(@"XShm not supported.");
no_xshm: 
        NSLog(@"Falling back to normal XImage (will be slower).");
        use_xshm = 0;
      }
    XSetErrorHandler(old_error_handler);
  }
}
#endif

@implementation XWindowBuffer

+ (void) initialize
{
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  use_shape_hack = [ud boolForKey: @"XWindowBuffer-shape-hack"];
}

+ windowBufferForWindow: (gswindow_device_t *)awindow
              depthInfo: (struct XWindowBuffer_depth_info_s *)aDI
{
  int i;
  XWindowBuffer *wi;
  int drawing_depth;
  Visual *visual;

  for (i = 0; i < num_window_buffers; i++)
    {
      if (window_buffers[i]->window == awindow)
        break;
    }
  if (i == num_window_buffers)
    {
      wi = [[XWindowBuffer alloc] init];
      wi->window = awindow;
      window_buffers = realloc(window_buffers,
        sizeof(XWindowBuffer *) * (num_window_buffers + 1));
      if (!window_buffers)
        {
          NSLog(@"Out of memory (failed to allocate %lu bytes)",
                (unsigned long)sizeof(XWindowBuffer *) * (num_window_buffers + 1));
          exit(1);
        }
      window_buffers[num_window_buffers++] = wi;
    }
  else
    {
      wi = window_buffers[i];
      wi = RETAIN(wi);
    }

  wi->DI = *aDI;
  wi->gc = awindow->gc;
  wi->drawable = awindow->ident;
  wi->display = awindow->display;

  wi->window->gdriverProtocol = GDriverHandlesExpose | GDriverHandlesBacking;
  wi->window->gdriver = wi;

#if 0
  drawing_depth = aDI->drawing_depth;
  visual = DefaultVisual(wi->display, DefaultScreen(wi->display));
#else
  // Better to get the used visual from the XGServer.
  visual = [(XGServer*)GSCurrentServer() screenVisual];
  drawing_depth = [(XGServer*)GSCurrentServer() screenDepth];
#endif

  /* TODO: resolve properly.
     -x11 is creating buffers before I have a chance to tell it not to, so
     I'm freeing them here to reduce memory consumption (and prevent
     leaks, though that should be fixed now) */
  if (awindow->buffer)
    {
      XFreePixmap (awindow->display, awindow->buffer);
      awindow->buffer = 0;
    }
  if (awindow->alpha_buffer)
    {
      XFreePixmap (awindow->display, awindow->alpha_buffer);
      awindow->alpha_buffer = 0;
    }

  /* create the image if necessary */
  if (!wi->ximage ||
      wi->sx != awindow->xframe.size.width ||
      wi->sy != awindow->xframe.size.height)
    {
/*                printf("%@ updating image for %p (%gx%g)\n", wi, wi->window,
                        wi->window->xframe.size.width, wi->window->xframe.size.height);*/
      if (wi->ximage)
        {
#ifdef XSHM
          if (wi->use_shm)
            {
              XShmDetach(wi->display, &wi->shminfo);
              XDestroyImage(wi->ximage);
              shmdt(wi->shminfo.shmaddr);
            }
          else
#endif
            XDestroyImage(wi->ximage);
        }
      if (wi->pixmap)
        {
          XFreePixmap(wi->display,wi->pixmap);
          XSetWindowBackground(wi->display,wi->window->ident,None);
          wi->pixmap=0;
        }

      wi->has_alpha = 0;
      if (wi->alpha)
        {
          free(wi->alpha);
          wi->alpha = NULL;
        }

      wi->pending_put = wi->pending_event = 0;

      wi->ximage = NULL;

      /* TODO: only use shared memory for 'real' on-screen windows. how can
      we tell? don't create shared buffer until first expose? */
      /* The primary problems seems to be the system limit on the _number_
      of shared memory segments, not their total size, so we only create
      shared buffers for reasonably large buffers and assume that the small
      ones are just caches of images and will never be displayed, anyway
      (and if they are displayed, it won't cost much, since they're small).
      */
      if (wi->window->xframe.size.width * wi->window->xframe.size.height < 4096)
        goto no_xshm;

#ifdef XSHM
      if (!did_test_xshm)
        test_xshm(wi->display, visual, drawing_depth);

      if (!use_xshm)
        goto no_xshm;

      /* Use XShm if possible, else fall back to normal XImage: s */
      wi->use_shm = 1;
      wi->ximage = XShmCreateImage(wi->display, visual,
                                   drawing_depth, ZPixmap, NULL, &wi->shminfo,
                                   wi->window->xframe.size.width,
                                   wi->window->xframe.size.height);
      if (!wi->ximage)
        {
          NSLog(@"Warning: XShmCreateImage failed!");
          NSLog(@"Falling back to normal XImage (will be slower).");
          goto no_xshm;
        }
      wi->shminfo.shmid = shmget(IPC_PRIVATE,
        wi->ximage->bytes_per_line * wi->ximage->height,
        IPC_CREAT | 0700);

      if (wi->shminfo.shmid == -1)
        {
          NSLog(@"Warning: shmget() failed: %m.");
          NSLog(@"Falling back to normal XImage (will be slower).");
          XDestroyImage(wi->ximage);
          goto no_xshm;
        }

      wi->shminfo.shmaddr = wi->ximage->data = shmat(wi->shminfo.shmid, 0, 0);
      if ((intptr_t)wi->shminfo.shmaddr == -1)
        {
          NSLog(@"Warning: shmat() failed: %m.");
          NSLog(@"Falling back to normal XImage (will be slower).");
          XDestroyImage(wi->ximage);
          shmctl(wi->shminfo.shmid, IPC_RMID, 0);
          goto no_xshm;
        }

      wi->shminfo.readOnly = 0;
      if (!XShmAttach(wi->display, &wi->shminfo))
        {
          NSLog(@"Warning: XShmAttach() failed.");
          NSLog(@"Falling back to normal XImage (will be slower).");
          XDestroyImage(wi->ximage);
          shmdt(wi->shminfo.shmaddr);
          shmctl(wi->shminfo.shmid, IPC_RMID, 0);
          goto no_xshm;
        }

      if (use_xshm_pixmaps)
        {
          /* We try to create a shared pixmap using the same buffer, and set
             it as the background of the window. This allows X to handle expose
             events all by itself, which avoids white flashing when things are
             dragged across a window. */
          /* TODO: we still get and handle expose events, although we don't
             need to. */
          wi->pixmap = XShmCreatePixmap(wi->display, wi->drawable,
                                        wi->ximage->data, &wi->shminfo,
                                        wi->window->xframe.size.width,
                                        wi->window->xframe.size.height,
                                        drawing_depth);
          if (wi->pixmap) /* TODO: this doesn't work */
            {
              XSetWindowBackgroundPixmap(wi->display, wi->window->ident,
                                         wi->pixmap);
            }
        }

      /* On some systems (eg. freebsd), X can't attach to the shared segment
      if it's marked for destruction, so we make sure it's attached before
      marking it. */
      XSync(wi->display, False);

      /* Mark the segment as destroyed now. Since we're attached, it won't
      actually be destroyed, but if we crashed before doing this, it wouldn't
      be destroyed despite nobody being attached anymore. */
      shmctl(wi->shminfo.shmid, IPC_RMID, 0);
#endif

      if (!wi->ximage)
        {
no_xshm: 
          wi->use_shm = 0;
          wi->ximage = XCreateImage(wi->display, visual, drawing_depth, 
                                    ZPixmap, 0, NULL,
                                    wi->window->xframe.size.width, 
                                    wi->window->xframe.size.height,
                                    8, 0);

	  /* Normally, the data of an XImage is saved with the X server's
	     byte order. However, some backends (notably cairo) use the
	     native byte order of the client for the backend image, hence
	     we must set the image's byte order here. Without this, GNUstep
	     applications would display wrong colors on an X server that
	     is running on a machine with a different byte order than the
	     client (bug #28590). */
	  wi->ximage->byte_order = wi->DI.byte_order;
          wi->ximage->data = malloc(wi->ximage->height * wi->ximage->bytes_per_line);
          if (!wi->ximage->data)
            {
              XDestroyImage(wi->ximage);
              wi->ximage = NULL;
            }
/*TODO?        wi->ximage = XGetImage(wi->display, wi->drawable,
                0, 0, wi->window->xframe.size.width, wi->window->xframe.size.height,
                -1, ZPixmap);*/
        }
    }

  if (wi->ximage)
    {
      wi->sx = wi->ximage->width;
      wi->sy = wi->ximage->height;
      wi->data = (unsigned char *)wi->ximage->data;
      wi->bytes_per_line = wi->ximage->bytes_per_line;
      wi->bits_per_pixel = wi->ximage->bits_per_pixel;
      wi->bytes_per_pixel = wi->bits_per_pixel / 8;
//                NSLog(@"%@ ximage=%p data=%p\n", wi->ximage, wi->data);
    }
  else
    {
      NSLog(@"Warning: failed to create image for window!");
      wi->data = NULL;
    }

  return AUTORELEASE(wi);
}


extern int XShmGetEventBase(Display *d);

- (void) _gotShmCompletion
{
#ifdef XSHM
  if (!use_shm)
    return;

  pending_event = 0;
  if (pending_put)
    {
      pending_put = 0;
      if (pending_rect.x + pending_rect.w > window->xframe.size.width)
        {
          pending_rect.w = window->xframe.size.width - pending_rect.x;
          if (pending_rect.w <= 0)
            return;
        }
      if (pending_rect.y + pending_rect.h > window->xframe.size.height)
        {
          pending_rect.h = window->xframe.size.height - pending_rect.y;
          if (pending_rect.h <= 0)
            return;
        }
      if (!XShmPutImage(display, drawable, gc, ximage,
                        pending_rect.x, pending_rect.y,
                        pending_rect.x, pending_rect.y,
                        pending_rect.w, pending_rect.h,
                        1))
        {
          NSLog(@"XShmPutImage failed?");
        }
      else
        {
          pending_event = 1;
        }
    }
//        XFlush(window->display);
#endif
}

- (void) _exposeRect: (NSRect)rect
{
/* TODO: Somehow, we can get negative coordinates in the rectangle. So far
I've tracked them back to [NSWindow flushWindow]. Should probably figure
out where they're coming from originally, and see if they really should be
negative. (Seems to happen when a window is created or resized, so possibly
something is refreshing while coordinates are still invalid.

Also, just about every resize of a window causes a few calls here with
rects in the new size before we are updated.

For now, we just intersect with our known size to avoid problems with X.

And, to avoid problems with clever optimizations and float vs. double
accuracy, we do the test using int: s.
*/
  int x, y, w, h;

  x = floor(rect.origin.x);
  y = floor(rect.origin.y);
  w = ceil(rect.size.width + rect.origin.x - x);
  h = ceil(rect.size.height + rect.origin.y - y);

  if (x < 0)
    {
      w += x;
      x = 0;
    }
  if (y < 0)
    {
      h += y;
      y = 0;
    }
  if (x + w > sx)
    {
      w = sx - x;
    }
  if (y + h > sy)
    {
      h = sy - y;
    }

  if (w <= 0 || h <= 0)
    return;

#ifdef XSHM
  if (use_shm)
    {
#ifdef HAVE_XSHAPE
      /* HACK: lets try to use shaped windows to get some use out of
         destination alpha */
      if (has_alpha && use_shape_hack)
        {
static int warn = 0;
          Pixmap p;
          int dsize = ((sx + 7) / 8) * sy;
          unsigned char *buf = malloc(dsize);
          unsigned char *dst;
          int bofs;
          unsigned char *a;
          int as;
          int i, x;

          if (!warn)
            NSLog(@"Warning: activating shaped windows");
          warn = 1;

          memset(buf, 0xff, dsize);

#define CUTOFF 128

          if (DI.inline_alpha)
            {
              a = data + DI.inline_alpha_ofs;
              as = DI.bytes_per_pixel;
            }
          else
            {
              a = alpha;
              as = 1;
            }

          for (bofs = 0, i = sx * sy, x = sx, dst = buf; i; i--, a += as)
            {
              if (*a < CUTOFF)
                {
                  *dst = *dst & ~(1 << bofs);
                }
              bofs++;
              if (bofs == 8)
                {
                  dst++;
                  bofs = 0;
                }
              x--;
              if (!x)
                {
                  if (bofs)
                    {
                      bofs = 0;
                      dst++;
                    }
                  x = sx;
                }
            }
#undef CUTOFF
//NSLog(@"check shape");
          if (old_shape_size == dsize && !memcmp(old_shape, buf, dsize))
            {
              free(buf);
//                NSLog(@"  same shape");
            }
          else
            {
//                NSLog(@"  updating");
              p = XCreatePixmapFromBitmapData(display, window->ident,
                                              (char *)buf, sx, sy, 1, 0, 1);
              free(old_shape);
              old_shape = buf;
              old_shape_size = dsize;
              XShapeCombineMask(display, window->ident,
                                ShapeBounding, 0, 0, p, ShapeSet);
              XFreePixmap(display, p);
            }
        }
#endif // HAVE_XSHAPE

      if (pending_event)
        {
          if (!pending_put)
            {
              pending_put = 1;
              pending_rect.x = x;
              pending_rect.y = y;
              pending_rect.w = w;
              pending_rect.h = h;
            }
          else
            {
              if (x < pending_rect.x)
                {
                  pending_rect.w += pending_rect.x - x;
                  pending_rect.x = x;
                }
              if (x + w > pending_rect.x + pending_rect.w)
                {
                  pending_rect.w = x + w - pending_rect.x;
                }
              if (y < pending_rect.y)
                {
                  pending_rect.h += pending_rect.y - y;
                  pending_rect.y = y;
                }
              if (y + h > pending_rect.y + pending_rect.h)
                {
                  pending_rect.h = y + h - pending_rect.y;
                }
            }
        }
      else
        {
          pending_put = 0;
          if (!XShmPutImage(display, drawable, gc, ximage,
                            x, y, x, y, w, h, 1))
            {
              NSLog(@"XShmPutImage failed?");
            }
          else
            {
              pending_event = 1;
            }
        }

      /* Performance hack. Check right away for ShmCompletion
         events. */
      {
        XEvent e;

        while (XCheckTypedEvent(window->display,
          XShmGetEventBase(window->display) + ShmCompletion, &e))
          {
            [[self class] _gotShmCompletion: ((XShmCompletionEvent *)&e)->drawable];
          }
      }
    }
  else
#endif
    if (ximage)
    {
      XPutImage(display, drawable, gc, ximage, x, y, x, y, w, h);
    }
}

- (void) needsAlpha
{
  if (has_alpha)
    return;

  if (!data)
    return;

//        NSLog(@"needs alpha for %p: %ix%i", self, sx, sy);

  if (DI.inline_alpha)
    {
      int i;
      unsigned char *s;
      alpha = NULL;
      has_alpha = 1;
      /* fill the alpha channel */
      for (i = 0, s = data + DI.inline_alpha_ofs; i < sx * sy;
           i++, s += DI.bytes_per_pixel)
        *s = 0xff;
      return;
    }

  alpha = malloc(sx * sy);
  if (!alpha)
    {
      NSLog(@"Warning! Failed to allocate alpha buffer for window!");
      return;
    }

//        NSLog(@"got buffer at %p", alpha);

  has_alpha = 1;
  memset(alpha, 0xff, sx * sy);
}

- (void) dealloc
{
  int i;

  for (i = 0; i < num_window_buffers; i++)
    if (window_buffers[i] == self) break;
  if (i < num_window_buffers)
    {
      num_window_buffers--;
      for (; i < num_window_buffers; i++)
        window_buffers[i] = window_buffers[i + 1];
    }

  if (ximage)
    {
      if (pixmap)
        {
          XFreePixmap(display,pixmap);
          pixmap=0;
        }

#ifdef XSHM
      if (use_shm)
        {
          XShmDetach(display, &shminfo);
          XDestroyImage(ximage);
          shmdt(shminfo.shmaddr);
        }
      else
#endif
        XDestroyImage(ximage);
    }
  if (alpha)
    free(alpha);
  [super dealloc];
}


+ (void) _gotShmCompletion: (Drawable)d
{
  int i;
  for (i = 0; i < num_window_buffers; i++)
    {
      if (window_buffers[i]->drawable == d)
        {
          [window_buffers[i] _gotShmCompletion];
          return;
        }
    }
}

@end

