/*
    pygame - Python Game Library
    Copyright (C) 2006, 2007 Rene Dudfield, Marcus von Appen

    Originally written and put in the public domain by Sam Lantinga.

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Library 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
    Library General Public License for more details.

    You should have received a copy of the GNU Library General Public
    License along with this library; if not, write to the Free
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#include <X11/Xutil.h>

static Display *SDL_Display;
static Window SDL_Window;
static void (*Lock_Display)(void);
static void (*Unlock_Display)(void);

/* Atoms used. */
static Atom _atom_UTF8;
static Atom _atom_TEXT;
static Atom _atom_COMPOUND;
static Atom _atom_MIME_PLAIN;
static Atom _atom_MIME_UTF8;
static Atom _atom_TARGETS;
static Atom _atom_TIMESTAMP;
static Atom _atom_SDL;
static Atom _atom_BMP;
static Atom _atom_CLIPBOARD;

/* Timestamps for the requests. */
static Time _cliptime = CurrentTime;
static Time _selectiontime = CurrentTime;

/* Maximum size to send or receive per request. */
#define MAX_CHUNK_SIZE(display)                                    \
    MIN(262144, /* 65536 * 4 */                                    \
        (XExtendedMaxRequestSize (display)) == 0                   \
        ? XMaxRequestSize (display) - 100                          \
        : XExtendedMaxRequestSize (display) - 100)

#define GET_CLIPATOM(x) ((x == SCRAP_SELECTION) ? XA_PRIMARY : _atom_CLIPBOARD)

static Atom _convert_format (char *type);
static void _init_atom_types (void);
static char* _atom_to_string (Atom a);
static void _add_clip_data (Atom type, char *data, int srclen);
static int _clipboard_filter (const SDL_Event *event);
static void _set_targets (PyObject *data, Display *display, Window window,
                          Atom property);
static int _set_data (PyObject *dict, Display *display, Window window,
                      Atom property, Atom target);
static Window _get_scrap_owner (Atom *selection);
static char* _get_data_as (Atom source, Atom format, unsigned long *length);

/**
 * \brief Converts the passed type into a system specific type to use
 *        for the clipboard.
 *
 * \param type The type to convert.
 * \return A system specific type.
 */
static Atom
_convert_format (char *type)
{
    if (strcmp (type, PYGAME_SCRAP_PPM) == 0)
        return XA_PIXMAP;
    if (strcmp (type, PYGAME_SCRAP_PBM) == 0)
        return XA_BITMAP;
    return XInternAtom (SDL_Display, type, False);
}

/**
 * \brief Initializes the used atom types.
 */
static void
_init_atom_types (void)
{
    _atom_UTF8 = XInternAtom (SDL_Display, "UTF8_STRING", False);
    _atom_TEXT = XInternAtom (SDL_Display, "TEXT", False);
    _atom_COMPOUND = XInternAtom (SDL_Display, "COMPOUND_TEXT", False);
    _atom_MIME_PLAIN = XInternAtom (SDL_Display, "text/plain", False);
    _atom_MIME_UTF8  = XInternAtom (SDL_Display, "text/plain;charset=utf-8",
                                    False);
    _atom_TARGETS = XInternAtom (SDL_Display, "TARGETS", False);
    _atom_TIMESTAMP = XInternAtom (SDL_Display, "TIMESTAMP", False);
    _atom_SDL = XInternAtom (SDL_Display, "SDL_SELECTION", False);
    _atom_BMP = XInternAtom (SDL_Display, PYGAME_SCRAP_BMP, False);
    _atom_CLIPBOARD = XInternAtom (SDL_Display, "CLIPBOARD", False);
}

/**
 * \brief Returns the name of the passed Atom. The result has to be
 * freed by the caller using free().
 *
 * \param a The Atom to get the name for.
 * \return The name of the Atom.
 */
static char*
_atom_to_string (Atom a)
{
    char *name;
    char *retval;

    if (!a)
        return NULL;
    name = XGetAtomName (SDL_Display, a);
    retval = strdup (name);
    XFree (name);
    return retval;
}

/**
 * Adds additional data to the currently selected clipboard and Window
 * if it does not already exist.
 *
 * \param cliptype The Atom to set the data for.
 * \param data The data to set.
 * \param srclen The length of the data.
 */
static void
_add_clip_data (Atom cliptype, char *data, int srclen)
{
    Atom clip = GET_CLIPATOM(_currentmode);
    PyObject* dict = (_currentmode == SCRAP_CLIPBOARD) ? _clipdata :
        _selectiondata;
    PyObject *tmp;
    char *key = _atom_to_string (cliptype);

    tmp = PyString_FromStringAndSize (data, srclen);
    PyDict_SetItemString (dict, key, tmp);
    Py_DECREF (tmp);
    XChangeProperty (SDL_Display, SDL_Window, clip, cliptype,
                     8, PropModeReplace, (unsigned char *) data, srclen);
    free (key);
}

/**
 * \brief System message filter function -- handles X11 clipboard messages.
 *
 * \param event The SDL_Event to check.
 * \return Always 1.
 */
static int
_clipboard_filter (const SDL_Event *event)
{
    PyObject *dict = NULL;
    Time timestamp = CurrentTime;

    /* Post all non-window manager specific events */
    if (event->type != SDL_SYSWMEVENT)
        return 1;

    XEvent xevent = event->syswm.msg->event.xevent;

    /* Handle window-manager specific clipboard events */
    switch (xevent.type)
    {
    case PropertyNotify:
    {
        /* Handled in pygame_scrap_put(). */
        break;
    }
    case SelectionClear:
    {
        XSelectionClearEvent *clear = &xevent.xselectionclear;

        /* Looks like another window takes control over the clipboard.
         * Release the internally saved buffer, if any.
         */
        if (clear->selection == XA_PRIMARY)
            timestamp = _selectiontime;
        else if(clear->selection == _atom_CLIPBOARD)
            timestamp = _cliptime;
        else
            break;

        /* Do not do anything, if the times do not match. */
        if (timestamp != CurrentTime && xevent.xselectionclear.time < timestamp)
            break;

        /* Clean the dictionaries. */
        if (clear->selection == XA_PRIMARY)
            PyDict_Clear (_selectiondata);
        else if (clear->selection != _atom_CLIPBOARD)
            PyDict_Clear (_clipdata);
        break;
    }
    case SelectionNotify:
        /* This one will be handled directly in the pygame_scrap_get ()
         * function.
         */
        break;

    case SelectionRequest:
    {
        XSelectionRequestEvent *req = &xevent.xselectionrequest;
        XEvent ev;
        
        /* Prepare answer. */
        ev.xselection.type      = SelectionNotify;
        ev.xselection.display   = req->display;
        ev.xselection.requestor = req->requestor;
        ev.xselection.selection = req->selection;
        ev.xselection.target    = req->target;
        ev.xselection.property  = None;
        ev.xselection.time      = req->time;

        /* Which clipboard type was requested? */
        if (req->selection == XA_PRIMARY)
        {
            dict = _selectiondata;
            timestamp = _selectiontime;
        }
        else if (req->selection == _atom_CLIPBOARD)
        {
            dict = _clipdata;
            timestamp = _cliptime;

        }
        else /* Anything else's not supported. */
        {
            XSendEvent (req->display, req->requestor, False, NoEventMask, &ev);
            return 1;
        }

        /* No data? */
        if (PyDict_Size (dict) == 0)
        {
            XSendEvent (req->display, req->requestor, False, NoEventMask, &ev);
            return 1;
        }

        /* We do not own the selection anymore. */
        if (timestamp == CurrentTime
            || (req->time != CurrentTime && timestamp > req->time))
        {
            XSendEvent (req->display, req->requestor, False, NoEventMask, &ev);
            return 1;
        }

        /* 
         * TODO: We have to make it ICCCM compatible at some point by
         * implementing the MULTIPLE atom request.
         */

        /* Old client? */
        if (req->property == None)
            ev.xselection.property = req->target;
        
        if (req->target == _atom_TARGETS)
        {
            /* The requestor wants to know, what we've got. */
            _set_targets (dict, req->display, req->requestor, req->property);
        }
        else
        {
            _set_data (dict, req->display, req->requestor, req->property,
                       req->target);
        }

        ev.xselection.property = req->property;
        /* Post the event for X11 clipboard reading above */
        XSendEvent (req->display, req->requestor, False, 0, &ev);
        break;
    }
    }
    return 1;
}

/**
 * Sets the list of target atoms available in the clipboard.
 *
 * \param data The clipboard dictionary.
 * \param display The requesting Display.
 * \param window The requesting Window.
 * \param property The request property to place the list into.
 */
static void
_set_targets (PyObject *data, Display *display, Window window, Atom property)
{
    int i;
    char *format;
    PyObject *list = PyDict_Keys (data);
    int amount = PyList_Size (list);
    /* All types plus the TARGETS and a TIMESTAMP atom. */
    Atom *targets = malloc ((amount + 2) * sizeof (Atom));
    if (targets == NULL)
        return;
    memset (targets, 0, (amount + 2) * sizeof (Atom));
    targets[0] = _atom_TARGETS;
    targets[1] = _atom_TIMESTAMP;
    for (i = 0; i < amount; i++)
    {
        format = PyString_AsString (PyList_GetItem(list, i));
        targets[i + 2] = _convert_format (format);
    }
    XChangeProperty (display, window, property, XA_ATOM, 32, PropModeReplace,
                     (unsigned char*) targets, amount + 2);
}

/**
 * Places the requested Atom data into a Window.
 *
 * \param data The clipboard dictionary.
 * \param display The requesting Display.
 * \param window The requesting Window.
 * \param property The request property to place the list into.
 * \param target The target porperty to place the list into.
 * \return 0 if no data for the target is available, 1 on success.
 */
static int
_set_data (PyObject *data, Display *display, Window window, Atom property,
           Atom target)
{
    char *name = _atom_to_string (target);
    PyObject *val = PyDict_GetItemString (data, name);
    char *value = NULL;
    int size;

    if (!val)
    {
        XFree (name);
        return 0;
    }
    size = PyString_Size (val);
    value = PyString_AsString (val);

    /* Send data. */
    XChangeProperty (display, window, property, target, 8, PropModeReplace,
                     (unsigned char *) value, size);
    XFree (name);
    return 1;
}

/**
 * \brief Tries to determine the X window with a valid selection.
 *        Default is to check
 *         - passed parameter
 *         - CLIPBOARD
 *         - XA_PRIMARY
 *         - XA_SECONDARY
 *         - XA_CUT_BUFFER0 - 7
 *        
 *         in this order.
 * 
 * \param selection The Atom type, that should be tried before any of the
 *                  fixed XA_* buffers.
 * \return The Window handle, that owns the selection or None if none was
 *         found.
 */
static Window
_get_scrap_owner (Atom *selection)
{
    int i = 0;
    static Atom buffers[] = { XA_PRIMARY, XA_SECONDARY, XA_CUT_BUFFER0,
                              XA_CUT_BUFFER1, XA_CUT_BUFFER2, XA_CUT_BUFFER3,
                              XA_CUT_BUFFER4, XA_CUT_BUFFER5, XA_CUT_BUFFER6,
                              XA_CUT_BUFFER7 };

    Window owner = XGetSelectionOwner (SDL_Display, *selection);
    if (owner != None)
        return owner;

    owner = XGetSelectionOwner (SDL_Display, _atom_CLIPBOARD);
    if (owner != None)
        return owner;

    while (i < 10)
    {
        owner = XGetSelectionOwner (SDL_Display, buffers[i]);
        if (owner != None)
        {
            *selection = buffers[i];
            return owner;
        }
        i++;
    }
    
    return None;
}

/**
 * Retrieves the data from a certain source Atom using a specific
 * format.
 *
 * \param source The currently active clipboard Atom to get the data from.
 * \param format The format of the data to get.
 * \param length Out parameter that contains the length of the returned
 * buffer.
 * \return The requested content or NULL in case no content exists or an
 * error occured.
 */
static char*
_get_data_as (Atom source, Atom format, unsigned long *length)
{
    char *retval = NULL;
    Window owner;
    time_t start;
    Atom sel_type;
    int sel_format;
    unsigned long nbytes;
    unsigned long overflow;
    *length = 0;
    unsigned char *src;
    unsigned long offset = 0;
    unsigned long chunk = 0;
    int step = 1;
    XEvent ev;
    Time timestamp;

    /* If we are the owner, simply return the clip buffer, if it matches
     * the request type.
     */
    if (!pygame_scrap_lost ())
    {
        char *fmt;
        char *data;

        fmt = _atom_to_string (format);

        if (_currentmode == SCRAP_SELECTION)
            data = PyString_AsString
                (PyDict_GetItemString (_selectiondata, fmt));
        else
            data = PyString_AsString (PyDict_GetItemString (_clipdata, fmt));
        free (fmt);

        return data;
    }

    Lock_Display ();
    
    /* Find a selection owner. */
    owner = _get_scrap_owner (&source);
    if (owner == None)
    {
        Unlock_Display ();
        return NULL;
    }

    timestamp = (source == XA_PRIMARY) ?  _selectiontime : _cliptime;

    /* Copy and convert the selection into our SDL_SELECTION atom of the
     * window. 
     * Flush afterwards, so we have an immediate effect and do not receive
     * the old buffer anymore.
     */
    XConvertSelection (SDL_Display, source, format, _atom_SDL, SDL_Window,
                       timestamp);
    XSync (SDL_Display, False);

    /* Let's wait for the SelectionNotify event from the callee and
     * react upon it as soon as it is received.
     */
    for (start = time (0);;)
    {
        if (XCheckTypedWindowEvent (SDL_Display, SDL_Window,
                                    SelectionNotify, &ev))
            break;
        if (time (0) - start >= 5)
        {
            /* Timeout, damn. */
            Unlock_Display ();
            return NULL;
        }
    }

    /* Get any property type and check the sel_type afterwards to decide
     * what to do.
     */
    if (XGetWindowProperty (SDL_Display, ev.xselection.requestor,
                            _atom_SDL, 0, 0, True,
                            AnyPropertyType, &sel_type, &sel_format,
                            &nbytes, &overflow, &src) != Success)
    {
        XFree (src);
        Unlock_Display ();
        return NULL;
    }

    /* In case we requested a SCRAP_TEXT, any property type of
     * XA_STRING, XA_COMPOUND_TEXT, UTF8_STRING and TEXT is valid.
     */
    if (format == _atom_MIME_PLAIN &&
        (sel_type != _atom_UTF8 && sel_type != _atom_TEXT
         && sel_type != _atom_COMPOUND && sel_type != XA_STRING))
    {
        /* No matching text type found. Return nothing then. */
        XFree (src);
        Unlock_Display ();
        return NULL;
    }

    /* Anything is fine, so copy the buffer and return it. */
    switch (sel_format)
    {
    case 16:
        step = sizeof (short) / 2;
        break;
    case 32:
        step = sizeof (long) / 4;
        break;
    case 8:
    default:
        step = sizeof (char);
        *length = overflow; /* 8 bit size is already correctly set in nbytes.*/
        break;
    }

    /* X11 guarantees NULL termination, add an extra byte. */
    *length = step * overflow;
    retval = malloc (*length + 1);
    if (retval)
    {
        unsigned long boffset = 0;
        chunk = MAX_CHUNK_SIZE(SDL_Display);
        memset (retval, 0, (size_t) (*length + 1));

        /* Read as long as there is data. */
        while (overflow)
        {
            if (XGetWindowProperty (SDL_Display, ev.xselection.requestor,
                                    _atom_SDL, offset, chunk, True,
                                    AnyPropertyType, &sel_type, &sel_format,
                                    &nbytes, &overflow, &src) != Success)
            {
                break;
            }
            
            offset += nbytes / (32 / sel_format);
            nbytes *= step * sel_format / 8;
            memcpy (retval + boffset, src, nbytes);
            boffset += nbytes;
            XFree (src);
        }
    }
    else
    {
        /* ENOMEM */
        return NULL;
    }

    /* In case we've got a COMPOUND_TEXT, convert it to the current
     * multibyte locale.
     */
    if (sel_type == _atom_COMPOUND && sel_format == 8)
    {
        char **list = NULL;
        int count;
        int status = 0;
        XTextProperty p;

        p.encoding = sel_type;
        p.format = sel_format;
        p.nitems = nbytes;
        p.value = retval;

        status = XmbTextPropertyToTextList (SDL_Display, &p, &list, &count);
        if (status == XLocaleNotSupported || status == XConverterNotFound)
        {
            free (retval);
            PyErr_SetString (PyExc_SDLError,
                             "current locale is not supported for conversion.");
            return NULL;
        }
        else if (status == XNoMemory)
        {
            free (retval);
            return NULL;
        }
        else if (status == Success)
        {
            if (count && list)
            {
                int i = 0;
                int ioffset = 0;
                char *tmp;

                free (retval);
                retval = NULL;
                for (i = 0; i < count; i++)
                {
                    *length = strlen (list[i]);
                    tmp = retval;
                    retval = realloc (retval, (*length) + 1);
                    if (!retval)
                    {
                        free (tmp);
                        return NULL;
                    }
                    ioffset += *length;

                    memcpy (retval, list[i], *length);
                    memset (retval + ioffset, '\n', 1);
                }
                memset (retval + ioffset, 0, 1);
            }
        }

        if (list)
            XFreeStringList (list);
    }

    Unlock_Display ();
    return retval;
}

int
pygame_scrap_init (void)
{
    SDL_SysWMinfo info;
    int retval = 0;

    /* Grab the window manager specific information */
    SDL_SetError ("SDL is not running on known window manager");

    SDL_VERSION (&info.version);
    if (SDL_GetWMInfo (&info))
    {
        /* Save the information for later use */
        if (info.subsystem == SDL_SYSWM_X11)
        {
            XWindowAttributes setattrs;
            XSetWindowAttributes newattrs;

            newattrs.event_mask = PropertyChangeMask;

            SDL_Display = info.info.x11.display;
            SDL_Window = info.info.x11.window;
            Lock_Display = info.info.x11.lock_func;
            Unlock_Display = info.info.x11.unlock_func;
            
            Lock_Display ();

            /* We need the PropertyNotify event for the timestap, so
             * modify the event attributes.
             */
            XGetWindowAttributes (SDL_Display, SDL_Window, &setattrs);
            newattrs.event_mask |= setattrs.all_event_masks;
            XChangeWindowAttributes
                (SDL_Display, SDL_Window, CWEventMask, &newattrs);

            Unlock_Display ();

            /* Enable the special window hook events */
            SDL_EventState (SDL_SYSWMEVENT, SDL_ENABLE);
            SDL_SetEventFilter (_clipboard_filter);

            /* Create the atom types we need. */
            _init_atom_types ();

            retval = 1;
        }
        else
            SDL_SetError ("SDL is not running on X11");
    }
    if (retval)
        _scrapinitialized = 1;

    return retval;
}

int
pygame_scrap_lost (void)
{
    int retval;

    if (!pygame_scrap_initialized ())
    {
        PyErr_SetString (PyExc_SDLError, "scrap system not initialized.");
        return 0;
    }

    Lock_Display ();
    retval = (XGetSelectionOwner (SDL_Display, GET_CLIPATOM (_currentmode))
              != SDL_Window);
    Unlock_Display ();

    return retval;
}

int
pygame_scrap_put (char *type, int srclen, char *src)
{
    Atom clip;
    Atom cliptype;
    Time timestamp = CurrentTime;
    time_t start;
    XEvent ev;

    if (!pygame_scrap_initialized ())
    {
        PyErr_SetString (PyExc_SDLError, "scrap system not initialized.");
        return 0;
    }

    Lock_Display ();

    clip = GET_CLIPATOM (_currentmode);
    cliptype = _convert_format (type);

    /* We've some types which should not be set by the user. */
    if (cliptype == _atom_TARGETS || cliptype == _atom_SDL
        || cliptype == _atom_TIMESTAMP)
    {
        PyErr_SetString (PyExc_ValueError, "the requested type is reserved.");
        Unlock_Display ();
        return 0;
    }

    /* Update the clipboard property with the buffer. */
    XChangeProperty (SDL_Display, SDL_Window, clip, cliptype,
                     8, PropModeReplace, (unsigned char *) src, srclen);

    if (cliptype == _atom_MIME_PLAIN)
    {
        /* Set PYGAME_SCRAP_TEXT. Also set XA_STRING, TEXT and
         * UTF8_STRING if they are not set in the dictionary.
         */
        _add_clip_data (XA_STRING, src, srclen);
        _add_clip_data (_atom_UTF8, src, srclen);
        _add_clip_data (_atom_TEXT, src, srclen);
    }
    XSync (SDL_Display, False);

    /* Update the timestamp */
    for (start = time (0);;)
    {
        if (XCheckTypedWindowEvent (SDL_Display, SDL_Window,
                                    PropertyNotify, &ev))
            break;
        if (time (0) - start >= 5)
        {
            /* Timeout, damn. */
            Unlock_Display ();
            goto SETSELECTIONOWNER;
        }
    }
    if (ev.xproperty.atom == clip)
    {
        timestamp = ev.xproperty.time;

        if (cliptype == XA_PRIMARY)
            _selectiontime = timestamp;
        else
            _cliptime = timestamp;
    }
    else
        timestamp = (cliptype == XA_PRIMARY) ? _selectiontime : _cliptime;

SETSELECTIONOWNER:
    /* Set the selection owner to the own window. */
    XSetSelectionOwner (SDL_Display, clip, SDL_Window, timestamp);
    if (XGetSelectionOwner (SDL_Display, clip) != SDL_Window)
    {
        /* Ouch, we could not toggle the selection owner. Raise an
         * error, as it's not guaranteed, that the clipboard
         * contains valid data.
         */
        Unlock_Display ();
        return 0;
    }

    Unlock_Display ();
    return 1;
}

char*
pygame_scrap_get (char *type, unsigned long *count)
{
    if (!pygame_scrap_initialized ())
    {
        PyErr_SetString (PyExc_SDLError, "scrap system not initialized.");
        return NULL;
    }
    return _get_data_as (GET_CLIPATOM (_currentmode),
                         _convert_format (type), count);
}

int
pygame_scrap_contains (char *type)
{
    int i = 0;
    char **types = pygame_scrap_get_types ();
    while (types[i])
    {
        if (strcmp (type, types[i]) == 0)
            return 1;
        i++;
    }
    return 0;
}

char**
pygame_scrap_get_types (void)
{
    char **types;
    Atom *targetdata;
    unsigned long length;
    
    if (!pygame_scrap_lost ())
    {
        PyObject *key;
        int pos = 0;
        int i = 0;
        PyObject *dict = (_currentmode == SCRAP_SELECTION) ? _selectiondata :
            _clipdata;
       
        types = malloc (sizeof (char*) * (PyDict_Size (dict) + 1));
        if (!types)
            return NULL;

        memset (types, 0, (size_t) (PyDict_Size (dict) + 1));
        while (PyDict_Next (dict, &pos, &key, NULL))
        {
            types[i] = strdup (PyString_AsString (key));
            if (!types[i])
            {
                /* Could not allocate memory, free anything. */
                int j = 0;
                while (types[j])
                {
                    free (types[j]);
                    j++;
                }
                free (types);
                return NULL;
            }
            i++;
        }
        types[i] = NULL;
        return types;
    }
    
    targetdata = (Atom *) _get_data_as (GET_CLIPATOM (_currentmode),
                                        _atom_TARGETS, &length);
    if (length > 0 && targetdata != NULL)
    {
        Atom *data =  targetdata;
        int count = length / sizeof (Atom);
        int i;
        char **targets  = malloc (sizeof (char *) * (count + 1));

        if (targets == NULL)
        {
            free (targetdata);
            return NULL;
        }
        memset (targets, 0, sizeof (char *) * (count + 1));

        for (i = 0; i < count; i++)
            targets[i] = _atom_to_string (data[i]);

        free (targetdata);
        return targets;
   }
    return NULL;
}
