/*-*-C-*-
 * Drag selections for the Menu CSE
 */

#include "resed.h"
#include "main.h"

#include "swicall.h"
#include "wimp.h"
#include "resformat.h"
#include "newmsgs.h"
#include "dragdrop.h"
#include "interactor.h"
#include "registry.h"

#include "menuedit.h"
#include "drag.h"
#include "props.h"
#include "protocol.h"


typedef struct
{
    MenuObjPtr menu;            /* Source menu */
    MenuObjPtr target;          /* Destination menu or NULL for none */
    PointRec work;
    unsigned int buttons, modifiers;
} DragClosureRec, *DragClosurePtr;


/*
 * Move the selected entries of 'src' onto a temporary list represented
 * by 'tmp'.  Do not recalculate the menu layout of src yet.  References
 * to malloced strings and editing dboxes are carried across.
 */

static void move_to_tmp (MenuObjPtr src, MenuEntryPtr *tmp)
{
    MenuEntryPtr entry, next;                    /* To step through entries */
    MenuEntryPtr left = NULL, lastleft = NULL;   /* To store new "src" list */
    MenuEntryPtr lasttmp = NULL;
    *tmp = NULL;

    for (entry = src->entries; entry; entry = next)
    {
        next = entry->next;
        entry->next = NULL;

        if (entry->selected)
        {
            /* Append to tmp */
            if (lasttmp)
                lasttmp->next = entry;
            else
                *tmp = entry;
            lasttmp = entry;
            src->numentries--;
        }
        else
        {
            /* Append to left */
            if (lastleft)
                lastleft->next = entry;
            else
                left = entry;
            lastleft = entry;
        }
    }
    src->numselected = 0;
    src->entries = left;                         /* Might be NULL */
}


/*
 * Copy the selected entries of 'src' onto a temporary list represented
 * by 'tmp'.  Malloced strings are cloned.  References to editing dboxes are
 * zapped to NULL in the copied objects.  If we run out of memory doing this
 * free the list so far built and return an error.
 */

static error * copy_to_tmp (MenuObjPtr src, MenuEntryPtr *tmp)
{
    MenuEntryPtr entry, next;                    /* To step through entries */
    MenuEntryPtr lasttmp = NULL, new;
    *tmp = NULL;

    for (entry = src->entries; entry; entry = entry->next)
    {
        if (entry->selected)
        {
            new = (MenuEntryPtr) malloc (sizeof (MenuEntryRec));
            if (new == NULL)
                goto fail;

            *new = *entry;
            new->next = NULL;

            /* Append to tmp */
            if (lasttmp)
                lasttmp->next = new;
            else
                *tmp = new;
            lasttmp = new;

            if (new->p.text)
            {
                new->p.text = copystring (new->p.text);
                if (new->p.text == NULL)
                {
                    new->p.helpmessage = NULL;
                    new->p.clickshow = NULL;
                    new->p.submenushow = NULL;
                    goto fail;
                }
            }

            if (new->p.helpmessage)
            {
                new->p.helpmessage = copystring (new->p.helpmessage);
                if (new->p.helpmessage == NULL)
                {
                    new->p.clickshow = NULL;
                    new->p.submenushow = NULL;
                    goto fail;
                }
            }

            if (new->p.clickshow)
            {
                new->p.clickshow = copystring (new->p.clickshow);
                if (new->p.clickshow == NULL)
                {
                    new->p.submenushow = NULL;
                    goto fail;
                }
            }
            if (new->p.submenushow)
            {
                new->p.submenushow = copystring (new->p.submenushow);
                if (new->p.submenushow == NULL)
                    goto fail;
            }

            new->dbox = NULL;
        }
    }
    return NULL;

fail:
    for (entry = *tmp; entry; entry = next)
    {
        next = entry->next;
        free (entry->p.text);
        free (entry->p.helpmessage);
        free (entry->p.clickshow);
        free (entry->p.submenushow);
        free ((char *) entry);
    }
    return error_lookup ("NoMem");
}


/*
 * Determine whether the given ID is used in an entry list or not.
 */

static Bool id_used (ComponentID id, MenuEntryPtr entry)
{
    for (; entry; entry = entry->next)
    {
        if (entry->separator)
            continue;
        if (entry->p.componentID == id)
            return TRUE;
    }
    return FALSE;
}


/*
 * Disambiguate the component IDs of tmp with respect to dst
 */

static void disambiguate (MenuEntryPtr within, MenuEntryPtr tmp)
{
    ComponentID nextID = 0;
    MenuEntryPtr entry;
    for (entry = within; entry; entry = entry->next)
        if (entry->separator == FALSE && entry->p.componentID >= nextID)
            nextID = entry->p.componentID + 1;
    for (entry = tmp; entry; entry = entry->next)
        if (entry->separator == FALSE && entry->p.componentID >= nextID)
            nextID = entry->p.componentID + 1;

    for (entry = tmp; entry; entry = entry->next)
    {
        if (entry->separator == FALSE && 
                (entry->p.componentID == -1 ||
                 id_used (entry->p.componentID, within)))
            entry->p.componentID = nextID++;
    }
}


/*
 * Make a note of the order of the entries in a menu.
 *
 * Result is NULL if memory runs out.
 */

static int * menu_entry_order_copy (MenuObjPtr menu)
{
    int n = menu->numentries;
    int *p = (int *) malloc ((n + 1) * sizeof(int));
    int i;
    MenuEntryPtr entry = menu->entries;

    if (p == NULL)
        return NULL;

    /* store number of entries at start */
    p[0] = n;

    /* copy component id's of the entries, with -1 to indicate a separator */
    for (i = 1; i <= n; i++)
    {
        p[i] = entry->separator ? -1 : entry->p.componentID;
        entry = entry->next;
    }

    return p;
}


/*
 * Compare the entry orders of the two items; result is TRUE iff they are
 *  the same.
 *
 * Space occupied by the copy 'old' is released.
 */

static Bool compare_menu_entry_order (int *old, MenuObjPtr new)
{
    int n = old[0];
    MenuEntryPtr entry = new->entries;
    int i;
    Bool ok = TRUE;

    /* compare number of entries first */
    if (n != new->numentries)
        ok = FALSE;
    else
        for (i = 1; i <= n; i++)
        {
            if (entry == NULL ||
                old[i] == -1 && !entry->separator ||
                old[i] != -1 &&
                   (entry->separator || old[i] != entry->p.componentID))
            {
                ok = FALSE;
                break;
            }
            entry = entry->next;
        }

    free (old);
    return ok;
}


/*
 * Copy or move the selection from src to dst, fix up display etc.
 * Where is the work-area coordinates on the dst where the drop happened.
 * There must be at least one selected entry in src.
 */

static error * finalise_drag (MenuObjPtr src, MenuObjPtr dst, PointPtr where, Bool move)
{
    MenuEntryPtr tmp, entry, last = NULL, after = NULL;
    int *oldsrc = NULL;
    int * olddst = NULL;
    error *err = NULL;

    if (dst->internal)
        return NULL;            /* can't drag to palette */
    if (src->internal)
        move = FALSE;           /* all drags from palette are copy */

    /* take a copy of the entry order in src */
    oldsrc = menu_entry_order_copy (src);
    if (oldsrc == NULL)
        return error_lookup ("NoMem");

    /* and of dst if necessary */
    if (src != dst)
    {
        olddst = menu_entry_order_copy (dst);
        if (olddst == NULL)
        {
            free (oldsrc);
            return error_lookup ("NoMem");
        }
    }

    if (move)
        move_to_tmp (src, &tmp);
    else
    {
        EG (fail, copy_to_tmp (src, &tmp) );
    }

    /* Disambiguate isn't very efficient the way I've implemented it, so avoid
     * it in the simple move case (where it is a no-op)
     */
    if (!move || src != dst)
        disambiguate (dst->entries, tmp);
    
    /*
     * Set after to the entry to insert after (or NULL for the beginning)
     * Insertion point is determined by seeing which entries the pointer
     * was between.
     */

    for (entry = dst->entries; entry; entry = entry->next)
    {
        if (where->y > (entry->miny + entry->maxy) / 2)
            break;
        after = entry;
    }

    /* Deselect all in dst.  This is a no-op for the simple move case */

    if (!move || src != dst)
    {
        for (entry = dst->entries; entry; entry = entry->next)
            entry->selected = FALSE;
        dst->numselected = 0;
    }

    /* Splice the tmp list into dst */
    
    for (entry = tmp; entry; entry = entry->next)
    {
        entry->owner = dst;
        dst->numselected++;
        last = entry;
    }

    if (after == NULL)
    {
        last->next = dst->entries;
        dst->entries = tmp;
    }
    else
    {
        last->next = after->next;
        after->next = tmp;
    }

    /* Ensure that any dboxes that may have been affected get updated */

    for (entry = dst->entries; entry; entry = entry->next)
        if (entry->selected && entry->dbox)
            (void) props_update_entry_dbox (entry, TRUE, TRUE);

    /* Remove invalid separators */

    menuedit_canonicalise (src);
    if (src != dst)
        menuedit_canonicalise (dst);

    /* Recalculate the positions etc of the menus */

    if (move)
    {
        EG ( fail, menuedit_justify_keycuts (src) );
        EG ( fail, menuedit_fix_extent (src) );
    }
    if (!move || src != dst)
    {
        EG ( fail, menuedit_justify_keycuts (dst) );
        EG ( fail, menuedit_fix_extent (dst) );
    }

    /* see if the order of the entries in src has changed */
    if (!compare_menu_entry_order (oldsrc, src))
    {
        EG ( fail, protocol_send_resed_object_modified (src) );
    }

    /* and check dst if appropriate */
    if (src != dst)
    {
        if (!compare_menu_entry_order (olddst, dst))
        {
            EG ( fail, protocol_send_resed_object_modified (dst) );
        }
    }

    /* Always claim the focus into the drop window */
    return menuedit_focus_claim (dst);

  fail:
    free (oldsrc);
    free (olddst);
    return err;
}


/*
 * Interactor for the drag operation
 */

static error * drag_interactor (unsigned int event, int *buf, void *closure, Bool *consumed)
{
    DragClosurePtr drag = (DragClosurePtr) closure;
    static Bool donepointer = FALSE;
    Bool removeptr;

    if (buf == NULL)            /* we are being asked to cancel */
    {
        if (donepointer)
        {
            dragdrop_normal_pointer ();
            donepointer = FALSE;
        }
        (void) dragdrop_scroll (NULL, NULL, NULL);
        return swi(Wimp_DragBox,  R1, 0,  END);
    }
    
    switch (event)
    {
    case EV_NULL_REASON_CODE:
        {
            PointerInfoRec pointer;
            RegistryType type;
            MenuObjPtr target;

            (void) swi (Wimp_GetPointerInfo,  R1, &pointer,  END);
            type = registry_lookup_window (pointer.windowhandle, (void **)&target);

            if (drag->target && (type != MenuEdit || target != drag->target))
            {
                if (donepointer)
                {
                    dragdrop_normal_pointer ();
                    donepointer = FALSE;
                }
                (void) dragdrop_scroll (NULL, NULL, NULL);
                drag->target = NULL;
            }

            if (type == MenuEdit)
            {
                drag->target = target;
                wimp_convert_point (ScreenToWork, drag->target->window,
                                    &pointer.position, &drag->work);
            }

            /* If we are not over a MenuEdit window then return */

            if (drag->target == NULL)
                return NULL;

            /* See if scrolling would be in order.  If it does scroll, then
             * the next thing received will be an Open Request which will
             * be handled in the usual way.
             */

            if (dragdrop_scroll (drag->target->window, &pointer.position, &removeptr))
            {
                if (donepointer == FALSE)
                {
                    dragdrop_scroll_pointer ();
                    donepointer = TRUE;
                }
            }
            if (donepointer && removeptr)
            {
                dragdrop_normal_pointer ();
                donepointer = FALSE;
            }
        }
        break;

    case EV_KEY_PRESSED:
        {
            KeyPressPtr key = (KeyPressPtr) buf;

            dragdrop_nudge (key->code, 4);

            /* drag has priority over keyboard shortcuts */
            *consumed = (key->code != 0x1b);
        }
        break;

    case EV_USER_DRAG_BOX:
        {
            interactor_cancel();
            *consumed = TRUE;
            if (drag->target)
            {
                /* It's a drag to one of our MenuEdit windows */
                Bool move = drag->modifiers & MODIFIER_SHIFT;
                if (drag->target == drag->menu)
                    move = !move;
                return finalise_drag (drag->menu, drag->target, &drag->work, move);
            }
        }
        break;
    }
    return NULL;
}


/*
 * Commence dragging the selected entries.  The coords in 'mouse' are screen-relative.
 * Note: the dragdrop calls are used to implement this because they give us
 * free auto-scrolling, etc.  Initially external drag-and-drop will not be
 * supported, but it could be added later.
 */

error * drag_start (MenuObjPtr menu, MenuEntryPtr entry,
                    MouseClickPtr mouse, unsigned int modifiers)
{
    static DragClosureRec closure;
    DragBoxRec box;
    
    closure.menu = menu;
    closure.target = NULL;
    closure.buttons = mouse->buttons;
    closure.modifiers = modifiers;

    box.type = 5;
    box.constrain.minx = box.constrain.miny = -BIG;
    box.constrain.maxx = box.constrain.maxy =  BIG;
    menuedit_get_selection_bbox (menu, &box.initial);
    wimp_convert_rect (WorkToScreen, menu->window, &box.initial, &box.initial);
    ER ( swi (Wimp_DragBox,  R1, &box,  END) );

    interactor_install (drag_interactor, (void *) &closure);
    interactor_enable_events (BIT(EV_NULL_REASON_CODE));
    interactor_set_timeout (5);
    return NULL;
}


/*
 * To keep commonlib.c.dragdrop happy re. linkage.  Replace with
 * a real one if use of MESSAGE_DRAGGING is reinstated in this program.
 */

error * dragdrop_accept (int windowhandle, MessageDraggingPtr msg, int *claimref)
{
    return NULL;
}
