/*
 * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javafx.embed.swing;

import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.application.Platform;
import javafx.scene.input.DataFormat;

import com.sun.javafx.tk.Toolkit;

import java.awt.Component;
import java.awt.Cursor;
import java.awt.EventQueue;
import java.awt.Image;
import java.awt.Point;
import java.awt.SecondaryLoop;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.FlavorTable;
import java.awt.datatransfer.SystemFlavorMap;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragGestureRecognizer;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceContext;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetContext;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.dnd.MouseDragGestureRecognizer;
import java.awt.dnd.InvalidDnDOperationException;
import java.awt.dnd.peer.DragSourceContextPeer;
import java.awt.dnd.peer.DropTargetContextPeer;

import java.io.IOException;
import java.io.UnsupportedEncodingException;

import java.util.Collections;
import java.util.ArrayList;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicBoolean;

import sun.awt.AWTAccessor;
import sun.awt.SunToolkit;
import sun.awt.dnd.SunDragSourceContextPeer;
import sun.awt.dnd.SunDropTargetEvent;
import sun.awt.datatransfer.DataTransferer;
import sun.awt.datatransfer.ToolkitThreadBlockedHandler;

import sun.swing.JLightweightFrame;


/**
 * A utility class to connect DnD mechanism of Swing and FX.
 * It allows Swing content to use the FX machinery for performing DnD.
 */
final class FXDnD {
    private final SwingNode node;
    private SwingNode getNode() { return node; }

    FXDnD(SwingNode node) {
        this.node = node;
    }

    /**
     * Utility class that operates on Maps with Components as keys.
     * Useful when processing mouse events to choose an object from the map
     * based on the component located at the given coordinates.
     */
    private class ComponentMapper<T> {
        private int x, y;
        private T object = null;

        private ComponentMapper(Map<Component, T> map, int xArg, int yArg) {
            this.x = xArg;
            this.y = yArg;

            final JLightweightFrame lwFrame = node.getLightweightFrame();
            Component c = AWTAccessor.getContainerAccessor().findComponentAt(
                    lwFrame, x, y, false);
            if (c == null) return;

            synchronized (c.getTreeLock()) {
                do {
                    object = map.get(c);
                } while (object == null && (c = c.getParent()) != null);

                if (object != null) {
                    // The object is either a DropTarget or a DragSource, so:
                    //assert c == object.getComponent();

                    // Translate x, y from lwFrame to component coordinates
                    while (c != lwFrame && c != null) {
                        x -= c.getX();
                        y -= c.getY();
                        c = c.getParent();
                    }
                }
            }
        }
    }
    public <T> ComponentMapper<T> mapComponent(Map<Component, T> map, int x, int y) {
        return new ComponentMapper<T>(map, x, y);
    }





    ///////////////////////////////////////////////////////////////////////////
    //     DRAG SOURCE IMPLEMENTATION
    ///////////////////////////////////////////////////////////////////////////


    private boolean isDragSourceListenerInstalled = false;

    // To keep track of where the DnD gesture actually started
    private MouseEvent pressEvent = null;
    private long pressTime = 0;

    private volatile SecondaryLoop loop;

    private final Map<Component, FXDragGestureRecognizer> recognizers = new HashMap<>();

    // Note that we don't really use the MouseDragGestureRecognizer facilities,
    // however some code in JDK may expect a descendant of this class rather
    // than a generic DragGestureRecognizer. So we inherit from it.
    private class FXDragGestureRecognizer extends MouseDragGestureRecognizer {
        FXDragGestureRecognizer(DragSource ds, Component c, int srcActions,
            DragGestureListener dgl)
        {
            super(ds, c, srcActions, dgl);

            if (c != null) recognizers.put(c, this);
        }

        @Override public void setComponent(Component c) {
            final Component old = getComponent();
            if (old != null) recognizers.remove(old);
            super.setComponent(c);
            if (c != null) recognizers.put(c, this);
        }

        protected void registerListeners() {
            SwingFXUtils.runOnFxThread(() -> {
                if (!isDragSourceListenerInstalled) {
                    node.addEventHandler(MouseEvent.MOUSE_PRESSED, onMousePressHandler);
                    node.addEventHandler(MouseEvent.DRAG_DETECTED, onDragStartHandler);
                    node.addEventHandler(DragEvent.DRAG_DONE, onDragDoneHandler);

                    isDragSourceListenerInstalled = true;
                }
            });
        }

        protected void unregisterListeners() {
            SwingFXUtils.runOnFxThread(() -> {
                if (isDragSourceListenerInstalled) {
                    node.removeEventHandler(MouseEvent.MOUSE_PRESSED, onMousePressHandler);
                    node.removeEventHandler(MouseEvent.DRAG_DETECTED, onDragStartHandler);
                    node.removeEventHandler(DragEvent.DRAG_DONE, onDragDoneHandler);

                    isDragSourceListenerInstalled = false;
                }
            });
        }

        private void fireEvent(int x, int y, long evTime, int modifiers) {
            // In theory we should register all the events that trigger the gesture (like PRESS, DRAG, DRAG, BINGO!)
            // But we can live with this hack for now.
            appendEvent(new java.awt.event.MouseEvent(getComponent(), java.awt.event.MouseEvent.MOUSE_PRESSED,
                        evTime, modifiers, x, y, 0, false));

            // Also, the modifiers here should've actually come from the last known mouse event (last MOVE or DRAG).
            // But we're OK with using the initial PRESS modifiers for now
            int initialAction = SunDragSourceContextPeer.convertModifiersToDropAction(
                    modifiers, getSourceActions());

            fireDragGestureRecognized(initialAction, new java.awt.Point(x, y));
        }
    }

    // Invoked on EDT
    private void fireEvent(int x, int y, long evTime, int modifiers) {
        ComponentMapper<FXDragGestureRecognizer> mapper = mapComponent(recognizers, x, y);

        final FXDragGestureRecognizer r = mapper.object;
        if (r != null) {
            r.fireEvent(mapper.x, mapper.y, evTime, modifiers);
        } else {
            // No recognizer, no DnD, no startDrag, so release the FX loop now
            SwingFXUtils.leaveFXNestedLoop(FXDnD.this);
        }
    }

    private MouseEvent getInitialGestureEvent() {
        return pressEvent;
    }

    private final EventHandler<MouseEvent> onMousePressHandler = (event) -> {
        // It would be nice to maintain a list of all the events that initiate
        // a DnD gesture (see a comment in FXDragGestureRecognizer.fireEvent().
        // For now, we simply use the initial PRESS event for this purpose.
        pressEvent = event;
        pressTime = System.currentTimeMillis();
    };


    private volatile FXDragSourceContextPeer activeDSContextPeer;

    private final EventHandler<MouseEvent> onDragStartHandler = (event) -> {
        // Call to AWT and determine the active DragSourceContextPeer
        activeDSContextPeer = null;
        final MouseEvent firstEv = getInitialGestureEvent();
        SwingFXUtils.runOnEDTAndWait(FXDnD.this, () -> fireEvent(
                    (int)firstEv.getX(), (int)firstEv.getY(), pressTime,
                    SwingEvents.fxMouseModsToMouseMods(firstEv)));
        if (activeDSContextPeer == null) return;

        // Since we're going to start DnD, consume the event.
        event.consume();

        Dragboard db = getNode().startDragAndDrop(SwingDnD.dropActionsToTransferModes(
                    activeDSContextPeer.sourceActions).toArray(new TransferMode[1]));

        // At this point the activeDSContextPeer.transferable contains all the data from AWT
        Map<DataFormat, Object> fxData = new HashMap<>();
        for (String mt : activeDSContextPeer.transferable.getMimeTypes()) {
            DataFormat f = DataFormat.lookupMimeType(mt);
            //TODO: what to do if f == null?
            if (f != null) fxData.put(f, activeDSContextPeer.transferable.getData(mt));
        }

        final boolean hasContent = db.setContent(fxData);
        if (!hasContent) {
            // No data, no DnD, no onDragDoneHandler, so release the AWT loop now
            loop.exit();
        }
    };

    private final EventHandler<DragEvent> onDragDoneHandler = (event) -> {
        event.consume();

        // Release FXDragSourceContextPeer.startDrag()
        loop.exit();

        if (activeDSContextPeer != null) {
            final TransferMode mode = event.getTransferMode();
            activeDSContextPeer.dragDone(
                    mode == null ? 0 : SwingDnD.transferModeToDropAction(mode),
                    (int)event.getX(), (int)event.getY());
        }
    };


    private final class FXDragSourceContextPeer extends SunDragSourceContextPeer {
        private volatile int sourceActions = 0;

        private final CachingTransferable transferable = new CachingTransferable();

        @Override public void startSecondaryEventLoop(){
            Toolkit.getToolkit().enterNestedEventLoop(this);
        }
        @Override public void quitSecondaryEventLoop(){
            assert !Platform.isFxApplicationThread();
            Platform.runLater(() -> Toolkit.getToolkit().exitNestedEventLoop(FXDragSourceContextPeer.this, null));
        }

        @Override protected void setNativeCursor(long nativeCtxt, Cursor c, int cType) {
            //TODO
        }


        private void dragDone(int operation, int x, int y) {
            dragDropFinished(operation != 0, operation, x, y);
        }

        FXDragSourceContextPeer(DragGestureEvent dge) {
            super(dge);
        }


        // It's Map<Long, DataFlavor> actually, but javac complains if the type isn't erased...
        @Override protected void startDrag(Transferable trans, long[] formats, Map formatMap)
        {
            activeDSContextPeer = this;

            // NOTE: we ignore the formats[] and the formatMap altogether.
            // AWT provides those to allow for more flexible representations of
            // e.g. text data (in various formats, encodings, etc.) However, FX
            // code isn't ready to handle those (e.g. it can't digest a
            // StringReader as data, etc.) So instead we perform our internal
            // translation.
            // Note that fetchData == true. FX doesn't support delayed data
            // callbacks yet anyway, so we have to fetch all the data from AWT upfront.
            transferable.updateData(trans, true);

            sourceActions = getDragSourceContext().getSourceActions();

            // Release the FX nested loop to allow onDragDetected to start the actual DnD operation,
            // and then start an AWT nested loop to wait until DnD finishes.
            loop = java.awt.Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop();
            SwingFXUtils.leaveFXNestedLoop(FXDnD.this);
            if (!loop.enter()) {
                // An error occured, but there's little we can do here...
            }
        }
    };

    public <T extends DragGestureRecognizer> T createDragGestureRecognizer(
            Class<T> abstractRecognizerClass,
            DragSource ds, Component c, int srcActions,
            DragGestureListener dgl)
    {
        return (T) new FXDragGestureRecognizer(ds, c, srcActions, dgl);
    }

    public DragSourceContextPeer createDragSourceContextPeer(DragGestureEvent dge) throws InvalidDnDOperationException
    {
        return new FXDragSourceContextPeer(dge);
    }





    ///////////////////////////////////////////////////////////////////////////
    //     DROP TARGET IMPLEMENTATION
    ///////////////////////////////////////////////////////////////////////////


    private boolean isDropTargetListenerInstalled = false;
    private volatile FXDropTargetContextPeer activeDTContextPeer = null;
    private final Map<Component, DropTarget> dropTargets = new HashMap<>();

    private final EventHandler<DragEvent> onDragEnteredHandler = (event) -> {
        if (activeDTContextPeer == null) activeDTContextPeer = new FXDropTargetContextPeer();

        int action = activeDTContextPeer.postDropTargetEvent(event);

        // If AWT doesn't accept anything, let parent nodes handle the event
        if (action != 0) event.consume();
    };

    private final EventHandler<DragEvent> onDragExitedHandler = (event) -> {
        if (activeDTContextPeer == null) activeDTContextPeer = new FXDropTargetContextPeer();

        activeDTContextPeer.postDropTargetEvent(event);

        activeDTContextPeer = null;
    };

    private final EventHandler<DragEvent> onDragOverHandler = (event) -> {
        if (activeDTContextPeer == null) activeDTContextPeer = new FXDropTargetContextPeer();

        int action = activeDTContextPeer.postDropTargetEvent(event);

        // If AWT doesn't accept anything, let parent nodes handle the event
        if (action != 0) {
            // NOTE: in FX the acceptTransferModes() may ONLY be called from DRAG_OVER.
            // If the AWT app always reports NONE and suddenly decides to accept the
            // data in its DRAG_DROPPED handler, this just won't work. There's no way
            // to workaround this other than by modifing the AWT application code.
            event.acceptTransferModes(SwingDnD.dropActionsToTransferModes(action).toArray(new TransferMode[1]));
            event.consume();
        }
    };

    private final EventHandler<DragEvent> onDragDroppedHandler = (event) -> {
        if (activeDTContextPeer == null) activeDTContextPeer = new FXDropTargetContextPeer();

        int action = activeDTContextPeer.postDropTargetEvent(event);

        if (action != 0) {
            // NOTE: the dropAction is ignored since we use the action last
            // reported from the DRAG_OVER handler.
            //
            // We might want to:
            //
            //    assert activeDTContextPeer.dropAction == onDragDroppedHandler.currentAction;
            //
            // and maybe print a diagnostic message if they differ.
            event.setDropCompleted(activeDTContextPeer.success);

            event.consume();
        }

        activeDTContextPeer = null;
    };

    private final class FXDropTargetContextPeer implements DropTargetContextPeer {

        private int targetActions = DnDConstants.ACTION_NONE;
        private int currentAction = DnDConstants.ACTION_NONE;
        private DropTarget dt = null;
        private DropTargetContext ctx = null;

        private final CachingTransferable transferable = new CachingTransferable();

        // Drop result
        private boolean success = false;
        private int dropAction = 0;

        @Override public synchronized void setTargetActions(int actions) { targetActions = actions; }
        @Override public synchronized int getTargetActions() { return targetActions; }

        @Override public synchronized DropTarget getDropTarget() { return dt; }

        @Override public synchronized boolean isTransferableJVMLocal() { return false; }

        @Override public synchronized DataFlavor[] getTransferDataFlavors() { return transferable.getTransferDataFlavors(); }
        @Override public synchronized Transferable getTransferable() { return transferable; }

        @Override public synchronized void acceptDrag(int dragAction) { currentAction = dragAction; }
        @Override public synchronized void rejectDrag() { currentAction = DnDConstants.ACTION_NONE; }

        @Override public synchronized void acceptDrop(int dropAction) { this.dropAction = dropAction; }
        @Override public synchronized void rejectDrop() { dropAction = DnDConstants.ACTION_NONE; }

        @Override public synchronized void dropComplete(boolean success) { this.success = success; }


        private int postDropTargetEvent(DragEvent event)
        {
            ComponentMapper<DropTarget> mapper = mapComponent(dropTargets, (int)event.getX(), (int)event.getY());

            final EventType<?> fxEvType = event.getEventType();

            Dragboard db = event.getDragboard();
            transferable.updateData(db, DragEvent.DRAG_DROPPED.equals(fxEvType));

            final int sourceActions = SwingDnD.transferModesToDropActions(db.getTransferModes());
            final int userAction = event.getTransferMode() == null ? DnDConstants.ACTION_NONE
                : SwingDnD.transferModeToDropAction(event.getTransferMode());

            // A target for the AWT DnD event
            DropTarget target = mapper.object != null ? mapper.object : dt;

            SwingFXUtils.runOnEDTAndWait(FXDnD.this, () -> {
                if (target != dt) {
                    if (ctx != null) ctx.removeNotify();
                    ctx = null;

                    currentAction = dropAction = DnDConstants.ACTION_NONE;
                }

                if (target != null) {
                    if (ctx == null) {
                        ctx = target.getDropTargetContext();
                        ctx.addNotify(FXDropTargetContextPeer.this);
                    }

                    DropTargetListener dtl = (DropTargetListener)target;

                    if (DragEvent.DRAG_DROPPED.equals(fxEvType)) {
                        DropTargetDropEvent awtEvent = new DropTargetDropEvent(
                            ctx, new Point(mapper.x, mapper.y), userAction, sourceActions);

                        dtl.drop(awtEvent);
                    } else {
                        DropTargetDragEvent awtEvent = new DropTargetDragEvent(
                            ctx, new Point(mapper.x, mapper.y), userAction, sourceActions);

                        if (DragEvent.DRAG_OVER.equals(fxEvType)) dtl.dragOver(awtEvent);
                        else if (DragEvent.DRAG_ENTERED.equals(fxEvType)) dtl.dragEnter(awtEvent);
                        else if (DragEvent.DRAG_EXITED.equals(fxEvType)) dtl.dragExit(awtEvent);
                    }
                }

                dt = mapper.object;
                if (dt == null) {
                    if (ctx != null) ctx.removeNotify();
                    ctx = null;

                    currentAction = dropAction = DnDConstants.ACTION_NONE;
                }
                if (DragEvent.DRAG_DROPPED.equals(fxEvType) || DragEvent.DRAG_EXITED.equals(fxEvType)) {
                    // This must be done to ensure that the data isn't being
                    // cached in AWT. Otherwise subsequent DnD operations will
                    // see the old data only.
                    if (ctx != null) ctx.removeNotify();
                    ctx = null;
                }

                SwingFXUtils.leaveFXNestedLoop(FXDnD.this);
            });

            if (DragEvent.DRAG_DROPPED.equals(fxEvType)) return dropAction;

            return currentAction;
        }
    }

    public void addDropTarget(DropTarget dt) {
        dropTargets.put(dt.getComponent(), dt);
        Platform.runLater(() -> {
            if (!isDropTargetListenerInstalled) {
                node.addEventHandler(DragEvent.DRAG_ENTERED, onDragEnteredHandler);
                node.addEventHandler(DragEvent.DRAG_EXITED, onDragExitedHandler);
                node.addEventHandler(DragEvent.DRAG_OVER, onDragOverHandler);
                node.addEventHandler(DragEvent.DRAG_DROPPED, onDragDroppedHandler);

                isDropTargetListenerInstalled = true;
            }
        });
    }

    public void removeDropTarget(DropTarget dt) {
        dropTargets.remove(dt.getComponent());
        Platform.runLater(() -> {
            if (isDropTargetListenerInstalled && dropTargets.isEmpty()) {
                node.removeEventHandler(DragEvent.DRAG_ENTERED, onDragEnteredHandler);
                node.removeEventHandler(DragEvent.DRAG_EXITED, onDragExitedHandler);
                node.removeEventHandler(DragEvent.DRAG_OVER, onDragOverHandler);
                node.removeEventHandler(DragEvent.DRAG_DROPPED, onDragDroppedHandler);

                isDropTargetListenerInstalled = true;
            }
        });
    }
}
