/* Copyright (c) 2007 Timothy Wall, All Rights Reserved
 *
 * The contents of this file is dual-licensed under 2
 * alternative Open Source/Free licenses: LGPL 2.1 or later and
 * Apache License 2.0. (starting with JNA version 4.0.0).
 *
 * You can freely decide which license you want to apply to
 * the project.
 *
 * You may obtain a copy of the LGPL License at:
 *
 * http://www.gnu.org/licenses/licenses.html
 *
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "LGPL2.1".
 *
 * You may obtain a copy of the Apache License at:
 *
 * http://www.apache.org/licenses/
 *
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "AL2.0".
 */
package com.sun.jna.platform.dnd;

import java.awt.Component;
import java.awt.Point;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetContext;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

/** Provides simplified drop handling for a component.
 * Usage:<br>
 * <pre><code>
 * int actions = DnDConstants.MOVE_OR_COPY;
 * Component component = ...;
 * DropHandler handler = new DropHandler(component, actions);
 * </code></pre>
 * <ul>
 * <li>Accept drops where the action is the default (i.e. no modifiers) but
 * the intersection of source and target actions is <i>not</i> the default.
 * Doing so allows the source to adjust the cursor appropriately.
 * <li>Refuse drops where the user modifiers request an action that is not
 * supported (this works for all cases except when the drag source is not
 * a {@link DragHandler} and the user explicitly requests a MOVE operation;
 * this is indistinguishable from a drag with no modifiers unless we have
 * access to the key modifiers, which {@link DragHandler} provides).
 * <li>Drops may be refused based on data flavor, location, intended drop
 * action, or any combination of those, by overriding {@link #canDrop}.
 * <li>Custom decoration of the drop area may be performed in
 * {@link #paintDropTarget(DropTargetEvent, int, Point)} or by providing
 * a {@link DropTargetPainter}.
 * </ul>
 *
 * The method {@link #getDropAction(DropTargetEvent)} follows these steps to
 * determine the appropriate action (if any).
 * <ul>
 * <li>{@link #isSupported(DataFlavor[])} determines if there are any supported
 * flavors
 * <li>{@link #getDropActionsForFlavors(DataFlavor[])} reduces the supported
 * actions based on available flavors.  For instance, a text field for file
 * paths might support {@link DnDConstants#ACTION_COPY_OR_MOVE} on a plain
 * string, but {@link DnDConstants#ACTION_LINK} might be the only action
 * supported on a file.
 * <li>{@link #getDropAction(DropTargetEvent, int, int, int)} relax the action
 * if it's the default, or restrict it for user requested actions.
 * <li>{@link #canDrop(DropTargetEvent, int, Point)} change the action based on
 * the location in the drop target component, or any other criteria.
 * </ul>
 *
 * Override {@link #drop(DropTargetDropEvent, int)} to handle the drop.
 * You should invoke {@link DropTargetDropEvent#dropComplete} as soon
 * as the {@link Transferable} data is obtained, to avoid making the DnD
 * operation look suspended.
 *
 * @see DragHandler
 * @author twall
 */
// NOTE: you could probably make one of these handlers serve several targets,
// but for simplicity, keep the mapping 1-1-1 handler/droptarget/component
// TODO: look into making use of the existing
// Transferable.SwingDropTarget on JComponent instances instead of
// creating a new DropTarget; we can add self as a listener; probably would
// want to remove the default TransferHandler.DropHandler, which uses
// the TransferHandler to drop
public abstract class DropHandler implements DropTargetListener {

    private static final Logger LOG = Logger.getLogger(DropHandler.class.getName());

    private int acceptedActions;
    private List<DataFlavor> acceptedFlavors;
    private DropTarget dropTarget;
    private boolean active = true;
    private DropTargetPainter painter;

    /** Create a handler that allows the given set of actions.  If using
     * this constructor, you will need to override {@link #isSupported} to
     * indicate which data flavors are allowed.
     * @param c target component
     * @param acceptedActions accepted actions
     */
    public DropHandler(Component c, int acceptedActions) {
        this(c, acceptedActions, new DataFlavor[0]);
    }

    /** Enable handling of drops, indicating what actions and flavors are
     * acceptable.
     * @param c The component to receive drops
     * @param acceptedActions Allowed actions for drops
     * @param acceptedFlavors Allowed data flavors for drops
     * @see #isSupported
     */
    public DropHandler(final Component c, int acceptedActions, DataFlavor[] acceptedFlavors) {
        this(c, acceptedActions, acceptedFlavors, null);
    }

    /** Enable handling of drops, indicating what actions and flavors are
     * acceptable, and providing a painter for drop target feedback.
     * @param c The component to receive drops
     * @param acceptedActions Allowed actions for drops
     * @param acceptedFlavors Allowed data flavors for drops
     * @param painter Painter to handle drop target feedback
     * @see #paintDropTarget
     */
    public DropHandler(final Component c, int acceptedActions,
                       DataFlavor[] acceptedFlavors, DropTargetPainter painter) {
        this.acceptedActions = acceptedActions;
        this.acceptedFlavors = Arrays.asList(acceptedFlavors);
        this.painter = painter;
        dropTarget = new DropTarget(c, acceptedActions, this, active);
    }

    protected DropTarget getDropTarget() {
        return dropTarget;
    }

    /**
     * @return Whether this drop target is active.
     */
    public boolean isActive() { return active; }

    /** Set whether this handler (and thus its drop target) will accept
     * any drops.
     * @param active whether this handler should accept drops.
     */
    public void setActive(boolean active) {
        this.active = active;
        if (dropTarget != null) {
            dropTarget.setActive(active);
        }
    }

    /** Indicate the actions available for the given list of data flavors.
     * Override this method if the acceptable drop actions depend
     * on the currently available {@link DataFlavor}.  The default returns
     * the accepted actions passed into the constructor.
     * @param dataFlavors currently available flavors
     * @return currently acceptable actions.
     * @see #getDropAction(DropTargetEvent, int, int, int)
     * @see #canDrop(DropTargetEvent, int, Point)
     */
    protected int getDropActionsForFlavors(DataFlavor[] dataFlavors) {
        return acceptedActions;
    }

    /** Calculate the effective action.  The default implementation
     * checks whether any {@link DataFlavor}s are supported, and if so,
     * will change the current action from {@link DnDConstants#ACTION_NONE} to
     * something in common between the source and destination.  Refuse
     * user-requested actions if they are not supported (rather than silently
     * accepting a non-user-requested action, which is the Java's DnD default
     * behavior).  The drop action is forced to {@link DnDConstants#ACTION_NONE}
     * if there is no supported data flavor.
     * @param e {@link DropTargetEvent}
     * @return effective drop action
     * @see #isSupported(DataFlavor[])
     * @see #getDropActionsForFlavors
     * @see #getDropAction(DropTargetEvent, int, int, int)
     * @see #canDrop(DropTargetEvent, int, Point)
     */
    protected int getDropAction(DropTargetEvent e) {
        int currentAction = DragHandler.NONE;
        int sourceActions = DragHandler.NONE;
        Point location = null;
        DataFlavor[] flavors = new DataFlavor[0];
        if (e instanceof DropTargetDragEvent) {
            DropTargetDragEvent ev = (DropTargetDragEvent)e;
            currentAction = ev.getDropAction();
            sourceActions = ev.getSourceActions();
            flavors = ev.getCurrentDataFlavors();
            location = ev.getLocation();
        }
        else if (e instanceof DropTargetDropEvent) {
            DropTargetDropEvent ev = (DropTargetDropEvent)e;
            currentAction = ev.getDropAction();
            sourceActions = ev.getSourceActions();
            flavors = ev.getCurrentDataFlavors();
            location = ev.getLocation();
        }
        if (isSupported(flavors)) {
            int availableActions = getDropActionsForFlavors(flavors);
            currentAction = getDropAction(e, currentAction, sourceActions, availableActions);
            if (currentAction != DragHandler.NONE) {
                if (canDrop(e, currentAction, location)) {
                    return currentAction;
                }
            }
        }
        return DragHandler.NONE;
    }

    /* Adjust the drop action depending on whether the
     * current action is the default or a specific user-requested action.
     * The default implementation will change the current action from
     * {@link DnDConstants#ACTION_NONE} if there are actions in
     * common between the source and destination.  It will refuse user-requested
     * actions if they are not supported (rather than silently accepting
     * a non-user-requested action, which is the behavior of Swing's default
     * drop handlers).<p>
     * You can override this method if you wish to adjust the action based
     * on the the drag location; if you wish to deny drops based on location,
     * override {@link #canDrop} instead.  If you wish to adjust
     * the action based on the available data flavors, override
     * {@link #getDropActionsForFlavor} instead.
     * @see #getDropActionsForFlavor
     * @see #canDrop(DropTargetEvent, int, Point)
     */
    protected int getDropAction(DropTargetEvent e, int currentAction,
                                int sourceActions, int acceptedActions) {
        boolean modifiersActive = modifiersActive(currentAction);
        if ((currentAction & acceptedActions) == DragHandler.NONE
            && !modifiersActive) {
            int action = acceptedActions & sourceActions;
            currentAction = action;
        }
        else if (modifiersActive) {
            int action = currentAction & acceptedActions & sourceActions;
            if (action != currentAction) {
                currentAction = action;
            }
        }
        return currentAction;
    }

    /** Returns whether there are key modifiers active ,
     * or false if they can't be determined.
     * We use the DragHandler hint, if available, or fall back to whether
     * the drop action is other than the default (move).
     * @param dropAction requested action.
     * @return whether any modifiers are active.
     */
    protected boolean modifiersActive(int dropAction) {
        int mods = DragHandler.getModifiers();
        if (mods == DragHandler.UNKNOWN_MODIFIERS) {
            if (dropAction == DragHandler.LINK
                || dropAction == DragHandler.COPY) {
                return true;
            }
            // Can't (yet) distinguish between a forced and a default move
            // without help from DragHandler
            return false;
        }
        return mods != 0;
    }

    private String lastAction;
    private void describe(String type, DropTargetEvent e) {
        if(LOG.isLoggable(Level.FINE)) {
            StringBuilder msgBuilder = new StringBuilder();
            msgBuilder.append("drop: ");
            msgBuilder.append(type);
            if (e instanceof DropTargetDragEvent) {
                DropTargetContext dtc = e.getDropTargetContext();
                DropTarget dt = dtc.getDropTarget();
                DropTargetDragEvent ev = (DropTargetDragEvent) e;
                msgBuilder.append(": src=");
                msgBuilder.append(DragHandler.actionString(ev.getSourceActions()));
                msgBuilder.append(" tgt=");
                msgBuilder.append(DragHandler.actionString(dt.getDefaultActions()));
                msgBuilder.append(" act=");
                msgBuilder.append(DragHandler.actionString(ev.getDropAction()));
            }
            else if (e instanceof DropTargetDropEvent) {
                DropTargetContext dtc = e.getDropTargetContext();
                DropTarget dt = dtc.getDropTarget();
                DropTargetDropEvent ev = (DropTargetDropEvent)e;
                msgBuilder.append(": src=");
                msgBuilder.append(DragHandler.actionString(ev.getSourceActions()));
                msgBuilder.append(" tgt=");
                msgBuilder.append(DragHandler.actionString(dt.getDefaultActions()));
                msgBuilder.append(" act=");
                msgBuilder.append(DragHandler.actionString(ev.getDropAction()));
            }
            String msg = msgBuilder.toString();
            if (!msg.equals(lastAction)) {
                LOG.log(Level.FINE, msg);
                lastAction = msg;
            }
        }
    }

    /** Accept or reject the drag represented by the given event.  Returns
     * the action determined by {@link #getDropAction(DropTargetEvent)}.
     * @param e event
     * @return resulting action
     */
    protected int acceptOrReject(DropTargetDragEvent e) {
        int action = getDropAction(e);
        if (action != DragHandler.NONE) {
            // NOTE: the action argument (as of 1.5+) is only passed
            // to the DropTargetContextPeer, *not* the drag source
            e.acceptDrag(action);
        }
        else {
            e.rejectDrag();
        }
        return action;
    }

    @Override
    public void dragEnter(DropTargetDragEvent e) {
        describe("enter(tgt)", e);
        int action = acceptOrReject(e);
        paintDropTarget(e, action, e.getLocation());
    }

    @Override
    public void dragOver(DropTargetDragEvent e) {
        describe("over(tgt)", e);
        int action = acceptOrReject(e);
        paintDropTarget(e, action, e.getLocation());
    }

    @Override
    public void dragExit(DropTargetEvent e) {
        describe("exit(tgt)", e);
        paintDropTarget(e, DragHandler.NONE, null);
    }

    @Override
    public void dropActionChanged(DropTargetDragEvent e) {
        describe("change(tgt)", e);
        int action = acceptOrReject(e);
        paintDropTarget(e, action, e.getLocation());
    }

    /** Indicates the user has initiated a drop.  The default performs all
     * standard drop validity checking and handling, then invokes
     * {@link #drop(DropTargetDropEvent,int)} if the drop looks acceptable.
     */
    @Override
    public void drop(DropTargetDropEvent e) {
        describe("drop(tgt)", e);
        int action = getDropAction(e);
        if (action != DragHandler.NONE) {
            e.acceptDrop(action);
            try {
                drop(e, action);
                // Just in case this hasn't been done yet
                e.dropComplete(true);
            } catch (Exception ex) {
                e.dropComplete(false);
            }
        } else {
            e.rejectDrop();
        }
        paintDropTarget(e, DragHandler.NONE, e.getLocation());
    }

    /** Return whether any of the flavors in the given list are accepted.
     * The list is compared against the accepted list provided in the
     * constructor.
     * @param flavors list of transfer flavors to check
     * @return whether any of the given flavors are supported
     */
    protected boolean isSupported(DataFlavor[] flavors) {
        Set<DataFlavor> set = new HashSet<DataFlavor>(Arrays.asList(flavors));
        set.retainAll(acceptedFlavors);
        return !set.isEmpty();
    }

    /** Update the appearance of the target component.  Normally the decoration
     * should be painted only if the event is an instance of
     * {@link DropTargetDragEvent} with an action that is not
     * {@link DragHandler#NONE}.  Otherwise the decoration should be removed
     * or hidden.
     * <p>
     * For an easy way to highlight the drop target, consider using a single
     * instance of <code>AbstractComponentDecorator</code> and moving it
     * according to the intended drop location.
     * @param e The drop target event
     * @param action The action for the drop
     * @param location The intended drop location, or null if there is none
     */
    protected void paintDropTarget(DropTargetEvent e, int action, Point location) {
        if (painter != null) {
            painter.paintDropTarget(e, action, location);
        }
    }

    /** Indicate whether the given drop action is acceptable at the given
     * location.  This method is the last check performed by
     * {@link #getDropAction(DropTargetEvent)}.
     * You may override this method to refuse drops on certain areas
     * within the drop target component.  The default always returns true.
     * @param e event
     * @param action requested action
     * @param location requested drop location
     * @return whether the drop is supported
     */
    protected boolean canDrop(DropTargetEvent e, int action, Point location) {
        return true;
    }

    /** Handle an incoming drop with the given action.  The action passed in
     * might be different from {@link DropTargetDropEvent#getDropAction},
     * for instance, if there are no modifiers and the default action is not
     * supported.  Calling {@link DropTargetDropEvent#dropComplete} is
     * recommended as soon as the {@link Transferable} data is obtained; this
     * allows the drag source to reset the cursor and any drag images which
     * may be in effect.
     * @param e event
     * @param action requested drop type
     * @throws UnsupportedFlavorException dropped item has no supported flavors
     * @throws IOException data access failure
     */
    protected abstract void drop(DropTargetDropEvent e, int action) throws UnsupportedFlavorException, IOException;
}