/*-*-C-*-
 * Drag selections for the Window 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 "format.h"
#include "relocate.h"
#include "windowedit.h"
#include "drag.h"
#include "gadget.h"
#include "grid.h"
#include "props.h"
#include "protocol.h"


typedef struct
{
    WindowObjPtr window;          /* Source window */
    PointRec start;               /* initial position in source work area */
    WindowObjPtr target;          /* Destination window or NULL for none */

  /* the following two fields are undefined unless target is non-NULL */
    PointRec offset;              /* position of mouse in target work area
                                      relative to start */
    RectRec dragbox;              /* work area coordinates of drag box when
                                      the drag started */

    unsigned int buttons;         /* mouse button(s) used for drag */
    unsigned int modifiers;       /* keys held down for drag */
    PointRec mousepos;            /* screen position of mouse */
} DragClosureRec, *DragClosurePtr;



/*
 * Move the selected gadgets of 'src' onto a temporary list represented
 *  by 'tmp'.
 * References to malloced strings and editing dboxes are carried across.
 */

static void move_to_tmp (WindowObjPtr src, GadgetPtr *tmp)
{
    GadgetPtr gadget, next;                   /* To step through gadgets */
    GadgetPtr left = NULL, lastleft = NULL;   /* To store new "src" list */
    GadgetPtr lasttmp = NULL;
    *tmp = NULL;

    for (gadget = src->gadgets; gadget; gadget = next)
    {
        next = gadget->next;
        gadget->next = NULL;

        if (gadget->selected)
        {
            /* Append to tmp */
            if (lasttmp)
                lasttmp->next = gadget;
            else
                *tmp = gadget;
            lasttmp = gadget;
            src->numgadgets--;
        }
        else
        {
            /* Append to left */
            if (lastleft)
                lastleft->next = gadget;
            else
                left = gadget;
            lastleft = gadget;
        }
    }
    src->numselected = 0;
    src->gadgets = left;
}


/*
 * Copy the selected gadgets 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 (WindowObjPtr src, GadgetPtr *tmp)
{
    GadgetPtr gadget, next;                    /* To step through gadgets */
    GadgetPtr lasttmp = NULL, new;
    *tmp = NULL;

    for (gadget = src->gadgets; gadget; gadget = gadget->next)
    {
        if (gadget->selected)
        {
            /* create a copy of the gadget's body and header */
            new = gadget_copy (gadget);
            if (new == NULL)
                goto fail;
           
            /* note: selected, dbox, next, owner etc. will be NULL */

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

fail:
    /* free those already copied */
    gadget = *tmp;
    while (gadget)
    {
        next = gadget->next;
        gadget_free (gadget);
        gadget = next;
    }

    return error_lookup ("NoMem");
}


/*
 * Determine whether the given ID is used in a gadget list or not.
 */

static Bool id_used (ComponentID id, GadgetPtr gadget)
{
    for (; gadget; gadget = gadget->next)
        if (gadget->hdr.componentID == id)
            return TRUE;

    return FALSE;
}


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

static void disambiguate (GadgetPtr within, GadgetPtr tmp)
{
    ComponentID nextID = 0;
    GadgetPtr gadget;

    for (gadget = within; gadget; gadget = gadget->next)
        if (gadget->hdr.componentID >= nextID)
            nextID = gadget->hdr.componentID + 1;
    for (gadget = tmp; gadget; gadget = gadget->next)
        if (gadget->hdr.componentID >= nextID)
            nextID = gadget->hdr.componentID + 1;

    for (gadget = tmp; gadget; gadget = gadget->next)
        if (gadget->hdr.componentID == -1 ||        /* ie from palette */
            id_used (gadget->hdr.componentID, within))
        {
            /* update any properties dbox that is open */
            if (gadget->dbox != NULL)
                gadget_dbox_update_componentid (gadget,
                                                gadget->hdr.componentID,
                                                nextID);
            gadget->hdr.componentID = nextID++;
        }

    return;
}


/*
 * Copy or move the selection from src to dst.
 * There will be at least one selected entry in src.
 */

static error * finalise_drag (
    WindowObjPtr src,   /* source editing window                   */
    PointPtr from,      /* work area coords of pick-up position    */
    WindowObjPtr dst,   /* destination editing window              */
    PointPtr delta,     /* offset of drop position                 */
    Bool move           /* TRUE => move, FALSE => copy             */
)
{
    GadgetPtr tmp;
    GadgetPtr gadget;
    RectRec bbox;

    /* enforce 4 OS unit rounding on distance moved */
    wimp_align_point (delta);

    /* nothing to do if no movement has occurred */
    if (src == dst && delta->x == 0 && delta->y == 0)
        return NULL;

    if (dst->internal)            /* can't drag to palette */
        return NULL;

    if (src->internal)            /* all drags from palette are copy */
        move = FALSE;

    /* round distance moved to nearest multiple of grid spacing if locked */
    if (src == dst && src->grid.lock)
        grid_snap_point (src, delta);

    /* invalidate that part of the source window which contains the original
     *  selection.
     */
    windowedit_get_selection_bbox (src, &bbox);
    /* must allow for ears, since src has focus now */
    windowedit_add_ears_to_bbox (&bbox, &bbox);
    wimp_invalidate (src->window, &bbox);

    /*
     * Treat simple move within a window as a special case; this is important
     *  because otherwise the order of gadgets within a window would change
     *  when they were moved around, with dire consequences for import of
     *  messages.
     */

    if (move && (src == dst))
    {
        /* move each selected gadget */
        gadget = src->gadgets;
        while (gadget != NULL)
        {
            if (gadget->selected)
            {
                gadget->hdr.bbox.minx += delta->x;
                gadget->hdr.bbox.maxx += delta->x;
                gadget->hdr.bbox.miny += delta->y;
                gadget->hdr.bbox.maxy += delta->y;
            }
            gadget = gadget->next;
        }
    }

    /*
     * Otherwise it's either a copy, or a move to another window - and both
     *  these cases require that the selected gadgets be copied/moved to
     *  another list.
     */

    else
    {
        /* invalidate that part of the destination window which contains its
         *  current selection - unless it is the same as the source window
         */
        if (src != dst)
        {
            windowedit_get_selection_bbox (dst, &bbox);
            /* no need to allow for ears since dst does not have focus now */
            wimp_invalidate (dst->window, &bbox);
        }

        /* extract relevant gadgets from source window */
        if (move)
            move_to_tmp (src, &tmp);
        else
            ER ( copy_to_tmp (src, &tmp) );

        /* ensure all gadgets have distinct componentids */
        if (!(move && src == dst))
            disambiguate (dst->gadgets, tmp);

        /* deselect all in dst - this is a no-op for the simple move case */
        if (!(move && src == dst))
        {
            for (gadget = dst->gadgets; gadget; gadget = gadget->next)
                gadget->selected = FALSE;
            dst->numselected = 0;
        }

        /*
         * If this is a move or copy of gadgets from one window to another we
         *  need to find out if there are any radio buttons involved, and, if
         *  so, we determine the smallest group number that exceeds all group
         *  numbers in use in both the destination window and the tmp list.
         * Then we renumber each group in the tmp list.
         */

        if (src != dst)
        {
            Bool hasradio = FALSE;
            int groupnum;

            gadget = tmp;
            while (gadget)
            {
                if (gadget->hdr.type == GADGET_RADIO_BUTTON)
                {
                    if (hasradio)
                    {
                        if (groupnum <= gadget->body.radiobutton.groupnumber)
                            groupnum =
                                gadget->body.radiobutton.groupnumber + 1;
                    }
                    else
                    {
                        groupnum = gadget->body.radiobutton.groupnumber + 1;
                        hasradio = TRUE;
                    }
                }

                gadget = gadget->next;
            }

            if (hasradio)
            {
                Bool foundone;
                int oldnum;
                int newnum = groupnum;

                /* make sure groupnum exceeds all group numbers in dst */
                gadget = dst->gadgets;
                while (gadget)
                {
                    if ( gadget->hdr.type == GADGET_RADIO_BUTTON &&
                         groupnum <= gadget->body.radiobutton.groupnumber )
                        groupnum = gadget->body.radiobutton.groupnumber + 1;
                    gadget = gadget->next;
                }

                /* scan tmp repeatedly renaming groups until none are left */
                do
                {
                    foundone = FALSE;
                    gadget = tmp;
                    while (gadget)
                    {
                        if (gadget->hdr.type == GADGET_RADIO_BUTTON)
                        {
                            if (foundone)
                            {
                                if (gadget->body.radiobutton.groupnumber
                                                                   == oldnum)
                                    gadget_rb_update_group (gadget, newnum);
                            }
                            else
                            {
                                if (gadget->body.radiobutton.groupnumber
                                                                  < groupnum)
                                {
                                    foundone = TRUE;
                                    oldnum =
                                        gadget->body.radiobutton.groupnumber;
                                    gadget_rb_update_group (gadget, newnum);
                                }
                            }
                        }
                        gadget = gadget->next;
                    }
                    newnum++;
                } while (foundone);
            }
        }

        /* Add the tmp gadget list to the dst window, and:
         *   - select each one
         *   - move each one
         */

        gadget = tmp;
        while (TRUE)
        {
            gadget->owner = dst;
            gadget->selected = TRUE;
            dst->numselected++;
            dst->numgadgets++;

            gadget->hdr.bbox.minx += delta->x;
            gadget->hdr.bbox.maxx += delta->x;
            gadget->hdr.bbox.miny += delta->y;
            gadget->hdr.bbox.maxy += delta->y;

            /* update window name field in properties dbox if necessary */
            if (move && gadget->dbox && src != dst)
                gadget_dbox_update_window_name (gadget, dst->name);

            /*
             * If the gadget is a default (or cancel) action button, and:
             *   - a copy is being made within the same window,  or
             *   - it is being moved or copied to another window,
             * then a search must be made of the destination window and any
             * other default (or cancel) action button must be unmarked as
             * such.
             */

            if ( gadget->hdr.type == GADGET_ACTION_BUTTON &&
                 (gadget->hdr.flags & ACTIONBUTTON_ISDEFAULT) &&
                 (src != dst || !move) )
                gadget_note_new_default_action_button (gadget);
            if ( gadget->hdr.type == GADGET_ACTION_BUTTON &&
                 (gadget->hdr.flags & ACTIONBUTTON_ISCANCEL) &&
                 (src != dst || !move) )
                gadget_note_new_cancel_action_button (gadget);
         
            /*
             * If it's a copy within one window, ensure any radio buttons are
             *  switched off. This makes sure that the newly-enlarged radio
             *  groups continue to have at most one button selected.
             */

            if ( gadget->hdr.type == GADGET_RADIO_BUTTON &&
                 (gadget->hdr.flags & RADIOBUTTON_ON) &&
                 !move &&
                 src == dst )
                gadget->hdr.flags &= ~RADIOBUTTON_ON;

            /*
             * Link in to dst's gadget list if we've reached the end.
             * The new gadgets are added at the end, so that previous gadget
             *  order is maintained as much as possible for message import to
             *  work.
             */

            if (gadget->next == NULL)
            {
                gadget = dst->gadgets;
                if (gadget == NULL)
                    dst->gadgets = tmp;
                else
                {
                    while (gadget->next != NULL)
                        gadget = gadget->next;
                    gadget->next = tmp;
                }
                break;
            }

            gadget = gadget->next;
        }
    }


    /* invalidate that part of the destination window which contains the
     *  moved/copied gadgets
     */
    windowedit_get_selection_bbox (dst, &bbox);
    /* allow for ears, since dst has or will have input focus */
    windowedit_add_ears_to_bbox (&bbox, &bbox);
    wimp_invalidate (dst->window, &bbox);

    /* note possible modifications */
    if (move)
        ER ( protocol_send_resed_object_modified (src) );
    if (dst)
        ER ( protocol_send_resed_object_modified (dst) );

    /* always claim the focus into the drop window */
    return windowedit_focus_claim (dst);
}


/*
 * Plot the drag box defined by the closure
 */

static void plot_drag_box (DragClosurePtr drag)
{
    RectRec box = drag->dragbox;

    box.minx += drag->offset.x;
    box.maxx += drag->offset.x;
    box.miny += drag->offset.y;
    box.maxy += drag->offset.y;
    wimp_update_eor_box (drag->target->window, &box);

    return;
}


/*
 * Circulate the drag box defined by the closure
 */

static void cycle_drag_box (DragClosurePtr drag)
{
    RectRec box = drag->dragbox;

    box.minx += drag->offset.x;
    box.maxx += drag->offset.x;
    box.miny += drag->offset.y;
    box.maxy += drag->offset.y;

    wimp_start_rotate_box ();
    wimp_update_eor_box (drag->target->window, &box);
    wimp_end_rotate_box ();

    return;
}


/*
 * Converts a screen mouse position into an offset from a work area origin.
 *
 * Takes account of grid lock if requested.
 */

static void convert_to_offset (
    WindowObjPtr window,
    Bool gridlock,
    PointPtr origin,
    PointPtr mouse,
    PointPtr offset
)
{
    wimp_convert_point (ScreenToWork, window->window, mouse, offset);

    offset->x -= origin->x;
    offset->y -= origin->y;

    /* force to grid spacing multiple if appropriate */
    if (gridlock && window->grid.lock)
        grid_snap_point (window, offset);

    return;
}


/*
 * 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);

        /* remove any drag box */
        if (drag->target != NULL)
            plot_drag_box (drag);

        /* cancel the drag */
        return swi(Wimp_DragBox,  R1, 0,  END);
    }
    
    switch (event)
    {
    case EV_NULL_REASON_CODE:
        {
            PointerInfoRec mouse;
            WindowObjPtr target;
            RegistryType type;
            PointRec offset;

            /* determine screen location of mouse */
            swi (Wimp_GetPointerInfo, R1, &mouse, END);

            /* and find out what's there */
            type = registry_lookup_window (mouse.windowhandle,
                                                 (void **) &target);
            if (type != WindowEdit)
                target = NULL;

            /* no further action needed if mouse remains outside windows */
            if (drag->target == NULL && target == NULL)
                break;


            /*
             * Either the mouse remains in the same window ...
             */

            if (drag->target == target)
            {
                PointRec oldscroll, newscroll;
                Bool abouttoscroll;

                /*
                 * Look to see if scrolling is about to happen.
                 * If so, then we wish only to remove the drag box: it will
                 *  be redrawn by the REDRAW_WINDOW_REQUEST code.
                 * The auto-scrolling routines actually update the scroll
                 *  offset, so we must preserve the original and reinstate
                 *  afterwards.
                 */

                oldscroll = target->window->scrolloffset;
                abouttoscroll = dragdrop_scroll (target->window,
                                                &mouse.position, &removeptr);
                if (abouttoscroll)
                {
                    /* take note of new scroll offset before restoring */
                    newscroll = target->window->scrolloffset;
                    target->window->scrolloffset = oldscroll;
                }

                /* convert mouse position to offset relative to start */
                convert_to_offset (target,
                                   target == drag->window,
                                   &drag->start,
                                   &mouse.position,
                                   &offset);

                /* redraw the drag box */
                if (offset.x == drag->offset.x &&
                    offset.y == drag->offset.y && !abouttoscroll)
                {
                    /* the box has not moved - just rotate it */
                    cycle_drag_box (drag);
                }
                else
                {
                    /* first remove the existing box */
                    plot_drag_box (drag);

                    /* move it */
                    drag->offset.x = offset.x;
                    drag->offset.y = offset.y;

                    /* and replot it - unless redraw code will do so */
                    if (!abouttoscroll)
                        plot_drag_box (drag);
                }

                if (abouttoscroll)
                {
                    /* restore new scroll offset ready for redraw code */
                    target->window->scrolloffset = newscroll;

                    if (donepointer == FALSE)
                    {
                        dragdrop_scroll_pointer ();
                        donepointer = TRUE;
                    }
                }
                if (donepointer && removeptr)
                {
                    dragdrop_normal_pointer ();
                    donepointer = FALSE;
                }

                /* that's it */
                break;
            }


            /*
             * ... otherwise the mouse is moving from one environment to
             *     another
             */

            /* if it's leaving a WindowEdit window */
            if (drag->target != NULL)
            {
                /* close down any autoscrolling in progress */
                if (donepointer)
                {
                    dragdrop_normal_pointer ();
                    donepointer = FALSE;
                }
                (void) dragdrop_scroll (NULL, NULL, NULL);

                /* and remove drag box from the screen */
                plot_drag_box (drag);

                /* if mouse is moving outside, must change drag type */
                if (target == NULL)
                {
                    DragBoxRec box;

                    /* kill user-assisted drag */
                    swi (Wimp_DragBox, R1, 0, END);

                    /* convert mouse position to offset from start */
                    convert_to_offset (drag->target,
                                       FALSE,   /* no grid lock */
                                       &drag->start,
                                       &mouse.position,
                                       &offset);

                    /* hence determining the initial work area position
                       of the drag box */
                    box.initial.minx = drag->dragbox.minx + offset.x;
                    box.initial.maxx = drag->dragbox.maxx + offset.x;
                    box.initial.miny = drag->dragbox.miny + offset.y;
                    box.initial.maxy = drag->dragbox.maxy + offset.y;

                    /* and thence its screen position */
                    wimp_convert_rect (WorkToScreen,
                                       drag->target->window,
                                       &box.initial,
                                       &box.initial);

                    /* start the wimp drag */
                    box.type = 5;
                    box.constrain.minx = box.constrain.miny = -BIG;
                    box.constrain.maxx = box.constrain.maxy =  BIG;

                    swi (Wimp_DragBox, R1, &box, END);

                    /* update the closure */
                    drag->target = NULL;

                    /* and that's it */
                    break;
                }
                else
                    /* just moving into another WindowEdit window */
                    drag->target = target;
            }

            /* otherwise it must be coming into a window from outside */
            else
            {
                DragBoxRec box;

                /* kill wimp-supported drag */
                swi (Wimp_DragBox, R1, 0, END);

                /* start user-assisted drag */
                box.type = 7;
                box.constrain.minx = box.constrain.miny = -BIG;
                box.constrain.maxx = box.constrain.maxy =  BIG;

                swi (Wimp_DragBox, R1, &box, END);

                /* update the closure */
                drag->target = target;
            }


            /*
             * At this point we know that the mouse has just (re)entered
             *  a WindowEdit window, and that the appropriate drag has
             *  been set up.
             * We have only to plot the drag box.
             */

            /* convert mouse position to offset relative to start */
            convert_to_offset (target,
                               target == drag->window,
                               &drag->start,
                               &mouse.position,
                               &offset);

            /* update offset and plot drag box */
            drag->offset.x = offset.x;
            drag->offset.y = offset.y;

            plot_drag_box (drag);
        }

        break;

    case EV_REDRAW_WINDOW_REQUEST:
        {
            WindowRedrawPtr redraw = (WindowRedrawPtr) buf;

            if (drag->target != NULL &&
                redraw->handle == drag->target->window->handle)
            {
                PointerInfoRec mouse;
                PointRec offset;

                *consumed = TRUE;

                /* redraw exposed part of window */
                windowedit_redraw_window (redraw, drag->target);

                /* determine revised position of drag box and draw it */
                swi (Wimp_GetPointerInfo, R1, &mouse, END);
                convert_to_offset (drag->target,
                                   drag->target == drag->window,
                                   &drag->start,
                                   &mouse.position,
                                   &offset);
                drag->offset.x = offset.x;
                drag->offset.y = offset.y;
                plot_drag_box (drag);

                /* note modification */
                protocol_send_resed_object_modified (drag->target);
            }
        }
        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 WindowEdit windows */
                Bool move = drag->modifiers & MODIFIER_SHIFT;

                if (drag->target == drag->window)
                    move = !move;

                return finalise_drag (drag->window, &drag->start,
                                      drag->target, &drag->offset,
                                      move);
            }
            else if (drag->window->numselected == 1)
            {
                PointerInfoRec pointer;
                RegistryType type;
                WindowObjPtr target;
                GadgetPtr gadget = drag->window->gadgets;

                /* find the dropped gadget */
                while (gadget != NULL && !gadget->selected)
                    gadget = gadget->next;

                /* find where it was dropped */
                (void) swi (Wimp_GetPointerInfo,  R1, &pointer,  END);
                type = registry_lookup_window (pointer.windowhandle,
                                                          (void **) &target);

                /* might be a drag of a single gadget to a writable icon
                    in a window or gadget properties dbox */
                if (type == MainpropsDbox)
                    return props_main_gadget_drop (target,
                                              pointer.iconhandle,
                                              gadget);
                else if (type == GadgetDbox)
                    return gadget_gadget_drop (
                               (GadgetPtr) (void *) target,
                               pointer.iconhandle,
                               gadget);
            }
        }
        break;
    }

    return NULL;
}


/*
 * Commence dragging the selected gadgets.  The coords in 'mouse' are
 *  screen-relative.
 *
 * Note: the auto-scrolling functions in dragdrop are used for convenience,
 *  but initially external drag-and-drop will not be supported; it could be
 *  added later if desired.
 */

error * drag_start (WindowObjPtr window, GadgetPtr gadget,
                    MouseClickPtr mouse, unsigned int modifiers)
{
    static DragClosureRec closure;
    DragBoxRec box;

    /* initialise closure for drag */    
    closure.window = window;
    closure.target = window;
    closure.buttons = mouse->buttons;
    closure.modifiers = modifiers;
    closure.mousepos = mouse->position;
    wimp_convert_point (ScreenToWork, window->window,
                                      &mouse->position, &closure.start);
    closure.offset.x = closure.offset.y = 0;
    windowedit_get_selection_bbox (window, &closure.dragbox);

    /* plot initial position of drag box */
    plot_drag_box (&closure);

    /* start up a user-assisted drag */
    box.type = 7;
    box.constrain.minx = box.constrain.miny = -BIG;
    box.constrain.maxx = box.constrain.maxy =  BIG;
    ER ( swi (Wimp_DragBox,  R1, &box,  END) );

    interactor_install (drag_interactor, (void *) &closure);
    interactor_enable_events (BIT(EV_NULL_REASON_CODE));
    interactor_set_timeout (2);

    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;
}
