/* 
 * Copyright (C) 2000-2004 the xine project
 * 
 * This file is part of xine, a unix video player.
 * 
 * xine 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.
 * 
 * xine 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110, USA
 *
 * $Id$
 *
 *
 * Thanks to Paul Sheer for his nice xdnd implementation in cooledit.
 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif /* HAVE_CONFIG_H */

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <X11/Xatom.h>
#ifdef HAVE_SYS_TIME_H
#include <sys/time.h> /* NetBSD needs this */
#endif
#include "common.h"
#include "xdnd.h"

#define XDND_VERSION 3

/* #undef DEBUG_DND */

/*
 * PRIVATES
 */

static int _is_atom_match(DndClass *xdnd, Atom **atom) {
  int i, j;
  
  for(i = 0; (*atom)[i] != 0; i++) {
    for(j = 0; j < MAX_SUPPORTED_TYPE; j++) {
      if((*atom)[i] == xdnd->supported[j])
	return i;
    }
  }

  return -1;
}

/*
 * Send XdndFinished to 'window' from target 'from'
 */
static void _dnd_send_finished (DndClass *xdnd, Window window, Window from) {
  XEvent xevent;

  if((xdnd == NULL) || (window == None) || (from == None))
    return;

  memset(&xevent, 0, sizeof(xevent));
  xevent.xany.type                  = ClientMessage;
  xevent.xany.display               = xdnd->display;
  xevent.xclient.window             = window;
  xevent.xclient.message_type       = xdnd->_XA_XdndFinished;
  xevent.xclient.format             = 32;
  XDND_FINISHED_TARGET_WIN(&xevent) = from;

  XLockDisplay(xdnd->display);
  XSendEvent(xdnd->display, window, 0, 0, &xevent);
  XUnlockDisplay(xdnd->display);
  xdnd->in_progress = False;
}

static void unescape_string(char *src, char *dest) {
  char *s, *d;

  if((src == NULL) || (dest == NULL)) {
    fprintf(stderr, "unescape got NULL argument(s)\n");
    return;
  }

  if(!strlen(src))
    return;

  s = src;
  d = dest;

  while(*s != '\0') {

    switch(*s) {
    case '%':
      if ((*(s) == '%') && (*(s + 1) != '%')) {
	char    buffer[5] = { '0', 'x', *(s + 1) , *(s + 2), '\0' };
	char   *p         = buffer;
	int     character = strtol(p, &p, 16);
	
	*d = character;
	s += 2;
      }
      else {
	*d++ = '%';
	*d = '%';
      }
      break;

    default:
      *d = *s;
      break;
    }
    s++;
    d++;
  }
  *d = '\0';
}

/*
 * WARNING: X unlocked function 
 */
static int _dnd_paste_prop_internal(DndClass *xdnd, Window from, Window insert,
				    Atom prop, Bool delete_prop) {
  long           nread;
  unsigned long  nitems;
  unsigned long  bytes_after;

  nread = 0;

  do {
    Atom      actual_type;
    int       actual_fmt;
    unsigned  char *s = NULL;

    if (XGetWindowProperty(xdnd->display, insert, prop, 
			nread / (sizeof(unsigned char *)), 65536,
			delete_prop, AnyPropertyType, 
			&actual_type, &actual_fmt, &nitems, &bytes_after,
			&s) != Success) 
	{
		if (s) XFree(s);
		return 1;
	}

    nread += nitems;
    if (!nread) {
      XFree(s);
      return 1;
    }

    /* Okay, got something, handle */
    if (*s != '\0') {
      /*
       * from manpage:
       * XGetWindowProperty always allocates one extra byte in prop_return
       * (even if the property is zero length) and sets it to zero so that
       * simple properties consisting of characters do not have to be copied
       * into yet another string before use.
       */
      char *p;
      int   plen;

      /* Extract all data, '\n' separated */
      p = strtok((char *)s, "\n");
      while (p != NULL) {

        plen = strlen(p) - 1;

        /* Cleanup end of string */
        while ((plen >= 0) && ((p[plen] == 10) || (p[plen] == 12) ||
                (p[plen] == 13)))
          p[plen--] = '\0';

        if (plen) {
          char *obuf;
          if (!strncmp(p, "file:", 5)) {
            obuf = (char *)safe_malloc(2*plen + 1);
            unescape_string(p, obuf);
          }
          else obuf = p;

#ifdef DEBUG_DND
          printf("GOT '%s'\n", obuf);
#endif

          if (xdnd->callback) xdnd->callback(obuf);
          if (obuf != p) {
            free(obuf);
            obuf = NULL;
          }
        }
        p = strtok(NULL, "\n");
      }
    }

    XFree(s);
  } while(bytes_after);

  return 0;
}

/*
 * Getting selections, using INCR if possible.
 */
static void _dnd_get_selection (DndClass *xdnd, Window from, Atom prop,
                                Window insert) {
  struct timeval  tv, tv_start;
  unsigned long   bytes_after;
  Atom            actual_type;
  int             actual_fmt;
  unsigned long   nitems;
  unsigned char  *s = NULL;

  if((xdnd == NULL) || (prop == None))
    return;

  XLockDisplay(xdnd->display);
  if(XGetWindowProperty(xdnd->display, insert, prop, 0, 8, False, AnyPropertyType, 
			&actual_type, &actual_fmt, &nitems, &bytes_after, &s) != Success) {
    XFree(s);
    XUnlockDisplay(xdnd->display);
    return;
  }

  XFree(s);

  if(actual_type != xdnd->_XA_INCR) {
    (void) _dnd_paste_prop_internal(xdnd, from, insert, prop, True);
    XUnlockDisplay(xdnd->display);
    return;
  }

  XDeleteProperty(xdnd->display, insert, prop);
  gettimeofday(&tv_start, 0);

  for(;;) {
    long    t;
    fd_set  r;
    XEvent  xe;

    if(XCheckMaskEvent(xdnd->display, PropertyChangeMask, &xe)) {
      if((xe.type == PropertyNotify) && (xe.xproperty.state == PropertyNewValue)) {

	/* time between arrivals of data */
	gettimeofday (&tv_start, 0);

	if(_dnd_paste_prop_internal(xdnd, from, insert, prop, True))
	  break;
      }
    } else {
      tv.tv_sec  = 0;
      tv.tv_usec = 10000;
      FD_ZERO(&r);
      FD_SET(ConnectionNumber(xdnd->display), &r);
      select(ConnectionNumber(xdnd->display) + 1, &r, 0, 0, &tv);

      if(FD_ISSET(ConnectionNumber(xdnd->display), &r))
	continue;
    }
    gettimeofday(&tv, 0);
    t = (tv.tv_sec - tv_start.tv_sec) * 1000000L + (tv.tv_usec - tv_start.tv_usec);

    /* No data for five seconds, so quit */
    if(t > 5000000L) {
      XUnlockDisplay(xdnd->display); 
      return;
    }
  }

  XUnlockDisplay(xdnd->display);
}

/*
 * Get list of type from window (more than 3).
 */
static void _dnd_get_type_list (DndClass *xdnd, Window window, Atom **typelist) {
  Atom            type, *a;
  int             format;
  unsigned long   count, remaining, i;
  unsigned char  *data = NULL;

  *typelist = NULL;

  if((xdnd == NULL) || (window == None))
    return;

  XLockDisplay(xdnd->display);
  XGetWindowProperty(xdnd->display, window, xdnd->_XA_XdndTypeList, 0, 0x8000000L, 
		     False, XA_ATOM, &type, &format, &count, &remaining, &data);

  XUnlockDisplay(xdnd->display);

  if((type != XA_ATOM) || (format != 32) || (count == 0) || (!data)) {

    if(data) {
      XLockDisplay(xdnd->display);
      XFree(data);
      XUnlockDisplay(xdnd->display);
    }

    fprintf(stderr, "xdnd.c@%d: XGetWindowProperty failed in "
                    "xdnd_get_type_list - dnd->_XA_XdndTypeList = %ld\n",
                    __LINE__, xdnd->_XA_XdndTypeList);
    return;
  }

  *typelist = (Atom *)safe_malloc((count + 1) * sizeof(Atom));
  a = (Atom *) data;

  for(i = 0; i < count; i++)
    (*typelist)[i] = a[i];

  (*typelist)[count] = 0;

  XLockDisplay(xdnd->display);
  XFree(data);
  XUnlockDisplay(xdnd->display);
}

/*
 * Get list of type from window (3).
 */
static void _dnd_get_three_types (XEvent * xevent, Atom **typelist) {
  int i;

  *typelist = (Atom *)safe_malloc((XDND_THREE + 1) * sizeof(Atom) );

  for(i = 0; i < XDND_THREE; i++)
    (*typelist)[i] = XDND_ENTER_TYPE(xevent, i);
  /* although (*typelist)[1] or (*typelist)[2] may also be set to nill */
  (*typelist)[XDND_THREE] = 0;	
}

/*
 * END OF PRIVATES
 */

/*
 * Initialize Atoms, ...
 */
void init_dnd(Display *display, DndClass *xdnd) {
char *prop_names[_XA_ATOMS_COUNT] = {
  "XdndAware", /* _XA_XdndAware */
  "XdndEnter", /* _XA_XdndEnter */
  "XdndLeave", /* _XA_XdndLeave */
  "XdndDrop", /* _XA_XdndDrop */
  "XdndPosition", /* _XA_XdndPosition */
  "XdndStatus", /* _XA_XdndStatus */
  "XdndSelection", /* _XA_XdndSelection */
  "XdndFinished", /* _XA_XdndFinished */
  "XdndTypeList", /* _XA_XdndTypeList */
  "INCR", /* _XA_INCR */
  "WM_DELETE_WINDOW", /* _XA_WM_DELETE_WINDOW */
  "TiMidityXSelWindowProperty" /* TIMIDITY_PROTOCOL_ATOM */
};

char *mime_names[MAX_SUPPORTED_TYPE] = {
 "text/uri-list", /* supported[0] */
 "text/plain"  /* supported[1] */
};


  XLockDisplay(display);

  XInternAtoms(display, prop_names, _XA_ATOMS_COUNT, False, xdnd->Atoms);
  XInternAtoms(display, mime_names, MAX_SUPPORTED_TYPE, False, xdnd->supported);

  XUnlockDisplay(display);

  xdnd->display			= display;
  xdnd->version			= XDND_VERSION;
  xdnd->callback		= NULL;
  xdnd->dragger_typelist	= NULL;
  xdnd->desired			= 0;
  xdnd->in_progress		= False;
}

/*
 * Add/Replace the XdndAware property of given window. 
 */
int make_window_dnd_aware(DndClass *xdnd, Window window,
                          dnd_callback_t cb) {
  Status        status;
  /* Because we don't install an alternate error handler,
   * we'll never get the error messages.
   */

  if(!xdnd->display)
    return 0;

  XLockDisplay(xdnd->display);
  status = XChangeProperty(xdnd->display, window, xdnd->_XA_XdndAware, XA_ATOM,
			   32, PropModeReplace, (unsigned char *)&xdnd->version, 1);
  XUnlockDisplay(xdnd->display);
  
  if((status == BadAlloc) || (status == BadAtom) || 
     (status == BadMatch) || (status == BadValue) || (status == BadWindow)) {
    fprintf(stderr, "XChangeProperty() failed.\n");
    return 0;
  }
  
  XLockDisplay(xdnd->display);
  XChangeProperty(xdnd->display, window, xdnd->_XA_XdndTypeList, XA_ATOM, 32,
		  PropModeAppend, (unsigned char *)&xdnd->supported, 1);
  XUnlockDisplay(xdnd->display);
  
  if((status == BadAlloc) || (status == BadAtom) || 
     (status == BadMatch) || (status == BadValue) || (status == BadWindow)) {
    fprintf(stderr, "XChangeProperty() failed.\n");
    return 0;
  }

  xdnd->callback = cb;
  xdnd->win = window; /* xdnd_listener will overwrite this */

  return 1;
}

/*
 * Handle ClientMessage/SelectionNotify events.
 */
int process_client_dnd_message(DndClass *xdnd, XEvent *event) {
  int retval = 0;

  if((xdnd == NULL) || (event == NULL))
    return 0;

  if(event->type == ClientMessage) {

    if((xdnd->in_progress == True) && (event->xclient.format == 32) && 
       (XDND_ENTER_SOURCE_WIN(event) == xdnd->_XA_WM_DELETE_WINDOW)) {
      XEvent xevent;

#ifdef DEBUG_DND
      printf("ClientMessage KILL\n");
#endif

      memset(&xevent, 0, sizeof(xevent));
      xevent.xany.type                 = DestroyNotify;
      xevent.xany.display              = xdnd->display;
      xevent.xdestroywindow.type       = DestroyNotify;
      xevent.xdestroywindow.send_event = True;
      xevent.xdestroywindow.display    = xdnd->display;
      xevent.xdestroywindow.event      = xdnd->win;
      xevent.xdestroywindow.window     = xdnd->win;

      XLockDisplay(xdnd->display);
      XSendEvent(xdnd->display, xdnd->win, True, 0L, &xevent);
      XUnlockDisplay(xdnd->display);

      retval = 1;
    }
    else if(event->xclient.message_type == xdnd->_XA_XdndEnter) {

#ifdef DEBUG_DND
      printf("XdndEnter\n");
#endif

      if((XDND_ENTER_VERSION(event) < 3) || 
         (xdnd->in_progress == True)) {
	return 0;
      }
      else xdnd->in_progress = True;

      xdnd->dragger_window   = XDND_ENTER_SOURCE_WIN(event);
      xdnd->dropper_toplevel = event->xany.window;
      xdnd->dropper_window   = None;

      free(xdnd->dragger_typelist);
      xdnd->dragger_typelist = NULL;

      if(XDND_ENTER_THREE_TYPES(event)) {
#ifdef DEBUG_DND
	printf("Three types only\n");
#endif
	_dnd_get_three_types(event, &xdnd->dragger_typelist);
      } 
      else {
#ifdef DEBUG_DND
	printf("More than three types - getting list\n");
#endif
	_dnd_get_type_list(xdnd, xdnd->dragger_window, &xdnd->dragger_typelist);
      }

      if(xdnd->dragger_typelist) {
	int atom_match;
#ifdef DEBUG_DND
	{
	  int   i;
	  for(i = 0; xdnd->dragger_typelist[i] != 0; i++) {
	    XLockDisplay(xdnd->display);
	    printf("%d: '%s' ", i, XGetAtomName(xdnd->display, xdnd->dragger_typelist[i]));
	    XUnlockDisplay(xdnd->display);
	    printf("\n");
	  }
	}
#endif

	if((atom_match = _is_atom_match(xdnd, &xdnd->dragger_typelist)) >= 0) {
	  xdnd->desired = xdnd->dragger_typelist[atom_match];
	}

      }
      else {
	fprintf(stderr,
        "xdnd.c@%d: xdnd->dragger_typelist is zero length!\n", __LINE__);
	/* Probably doesn't work */
	if ((event->xclient.data.l[1] & 1) == 0) {
	  xdnd->desired = (Atom) event->xclient.data.l[1];
	}
      }
      retval = 1;
    }
    else if(event->xclient.message_type == xdnd->_XA_XdndLeave) {
#ifdef DEBUG_DND
      printf("XdndLeave\n");
#endif

      if((event->xany.window == xdnd->dropper_toplevel) && (xdnd->dropper_window != None))
	event->xany.window = xdnd->dropper_window;

      if(xdnd->dragger_window == XDND_LEAVE_SOURCE_WIN(event)) {
	free(xdnd->dragger_typelist);
	xdnd->dragger_typelist = NULL;
	xdnd->dropper_toplevel = xdnd->dropper_window = None;
	xdnd->desired = 0;
        xdnd->in_progress = False;
      }

      retval = 1;
    } 
    else if(event->xclient.message_type == xdnd->_XA_XdndDrop) {
      Window  win;

#ifdef DEBUG_DND
      printf("XdndDrop\n");
#endif
      if(xdnd->dragger_window == XDND_DROP_SOURCE_WIN(event)) {

      if(xdnd->desired != 0) {

	if((event->xany.window == xdnd->dropper_toplevel) && (xdnd->dropper_window != None))
	  event->xany.window = xdnd->dropper_window;
	
	  xdnd->time = XDND_DROP_TIME (event);

	  XLockDisplay(xdnd->display);
	  if(!(win = XGetSelectionOwner(xdnd->display,
                                        xdnd->_XA_XdndSelection)))
          {
	    fprintf(stderr,
                      "xdnd.c@%d: XGetSelectionOwner() failed.\n", __LINE__);
	    XUnlockDisplay(xdnd->display);
	    return 0;
	  }

	  XConvertSelection(xdnd->display, xdnd->_XA_XdndSelection,
          xdnd->desired, xdnd->TIMIDITY_PROTOCOL_ATOM,
          xdnd->dropper_toplevel, xdnd->time);
	  XUnlockDisplay (xdnd->display);
	}

         _dnd_send_finished(xdnd, xdnd->dragger_window,
         xdnd->dropper_toplevel);
      }

      retval = 1;
    }
    else if(event->xclient.message_type == xdnd->_XA_XdndPosition) {
      XEvent  xevent;
      Window  parent, child, new_child;

#ifdef DEBUG_DND
      printf("XdndPosition\n");
#endif

      XLockDisplay(xdnd->display);

#if 0
      toplevel = event->xany.window;
#endif
      parent   = DefaultRootWindow(xdnd->display);
      child    = xdnd->dropper_toplevel;

      for(;;) {
	int xd, yd;
	
	new_child = None;
	if(!XTranslateCoordinates (xdnd->display, parent, child, 
				   XDND_POSITION_ROOT_X(event), XDND_POSITION_ROOT_Y(event),
				   &xd, &yd, &new_child))
	  break;

	if(new_child == None)
	  break;

	child = new_child;
      }

      XUnlockDisplay(xdnd->display);

      xdnd->dropper_window = event->xany.window = child;

      xdnd->x    = XDND_POSITION_ROOT_X(event);
      xdnd->y    = XDND_POSITION_ROOT_Y(event);
      xdnd->time = XDND_POSITION_TIME(event);

      memset (&xevent, 0, sizeof(xevent));
      xevent.xany.type            = ClientMessage;
      xevent.xany.display         = xdnd->display;
      xevent.xclient.window       = xdnd->dragger_window;
      xevent.xclient.message_type = xdnd->_XA_XdndStatus;
      xevent.xclient.format       = 32;

      XDND_STATUS_TARGET_WIN(&xevent) = xdnd->dropper_toplevel;
      XDND_STATUS_WILL_ACCEPT_SET(&xevent, True);
      XDND_STATUS_WANT_POSITION_SET(&xevent, True);
      XDND_STATUS_RECT_SET(&xevent, xdnd->x, xdnd->y, 1, 1);
      XDND_STATUS_ACTION(&xevent) = XDND_POSITION_ACTION(event);

      XLockDisplay(xdnd->display);
      XSendEvent(xdnd->display, xdnd->dragger_window, 0, 0, &xevent);
      XUnlockDisplay(xdnd->display);
    }

    retval = 1;
  }
  else if(event->type == SelectionNotify) {

#ifdef DEBUG_DND
      printf("SelectionNotify\n");
#endif

    if(event->xselection.property == xdnd->TIMIDITY_PROTOCOL_ATOM) {
      _dnd_get_selection(xdnd, xdnd->dragger_window, 
			 event->xselection.property, event->xany.window);
      _dnd_send_finished(xdnd, xdnd->dragger_window, xdnd->dropper_toplevel);
    } 

    free(xdnd->dragger_typelist);
    xdnd->dragger_typelist = NULL;

    retval = 1;
  }

  return retval;
}
