/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jface.util;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.swt.dnd.DragSource;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DragSourceListener;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.dnd.TransferData;

/**
 * A <code>DelegatingDragAdapter</code> is a <code>DragSourceListener</code>
 * that maintains and delegates to a set of {@link TransferDragSourceListener}s.
 * Each TransferDragSourceListener can then be implemented as if it were the
 * <code>DragSource's</code> only DragSourceListener.
 * <p>
 * When a drag is started, a subset of all
 * <code>TransferDragSourceListeners</code> is generated and stored in a list of
 * <i>active</i> listeners. This subset is calculated by forwarding
 * {@link DragSourceListener#dragStart(DragSourceEvent)} to every listener, and
 * checking if the {@link DragSourceEvent#doit doit} field is left set to
 * <code>true</code>.
 * </p>
 * The <code>DragSource</code>'s set of supported Transfer types
 * ({@link DragSource#setTransfer(Transfer[])}) is updated to reflect the
 * Transfer types corresponding to the active listener subset.
 * <p>
 * If and when {@link #dragSetData(DragSourceEvent)} is called, a single
 * <code>TransferDragSourceListener</code> is chosen, and only it is allowed to
 * set the drag data. The chosen listener is the first listener in the subset of
 * active listeners whose Transfer supports
 * ({@link Transfer#isSupportedType(TransferData)}) the <code>dataType</code> in
 * the <code>DragSourceEvent</code>.
 * </p>
 * <p>
 * The following example snippet shows a <code>DelegatingDragAdapter</code> with
 * two <code>TransferDragSourceListeners</code>. One implements drag of text
 * strings, the other supports file transfer and demonstrates how a listener can
 * be disabled using the dragStart method.
 * </p>
 *
 * <pre>
 * <code>
 *		final TreeViewer viewer = new TreeViewer(shell, SWT.NONE);
 *
 *		DelegatingDragAdapter dragAdapter = new DelegatingDragAdapter();
 *		dragAdapter.addDragSourceListener(new TransferDragSourceListener() {
 *			public Transfer getTransfer() {
 *				return TextTransfer.getInstance();
 *			}
 *			public void dragStart(DragSourceEvent event) {
 *				// always enabled, can control enablement based on selection etc.
 *			}
 *			public void dragSetData(DragSourceEvent event) {
 *				event.data = "Transfer data";
 *			}
 *			public void dragFinished(DragSourceEvent event) {
 *				// no clean-up required
 *			}
 *		});
 *		dragAdapter.addDragSourceListener(new TransferDragSourceListener() {
 *			public Transfer getTransfer() {
 *				return FileTransfer.getInstance();
 *			}
 *			public void dragStart(DragSourceEvent event) {
 *				// enable drag listener if there is a viewer selection
 *				event.doit = !viewer.getSelection().isEmpty();
 *			}
 *			public void dragSetData(DragSourceEvent event) {
 *				File file1 = new File("C:/temp/file1");
 *				File file2 = new File("C:/temp/file2");
 *				event.data = new String[] {file1.getAbsolutePath(), file2.getAbsolutePath()};
 *			}
 *			public void dragFinished(DragSourceEvent event) {
 *				// no clean-up required
 *			}
 *		});
 *		viewer.addDragSupport(DND.DROP_COPY | DND.DROP_MOVE, dragAdapter.getTransfers(), dragAdapter);
 * </code>
 * </pre>
 *
 * @since 3.0
 */
public class DelegatingDragAdapter implements DragSourceListener {
	private List<TransferDragSourceListener> listeners = new ArrayList<>();

	private List<TransferDragSourceListener> activeListeners = new ArrayList<>();

	private TransferDragSourceListener currentListener;

	/**
	 * Adds the given <code>TransferDragSourceListener</code>.
	 *
	 * @param listener the new listener
	 */
	public void addDragSourceListener(TransferDragSourceListener listener) {
		listeners.add(listener);
	}

	/**
	 * The drop has successfully completed. This event is forwarded to the current
	 * drag listener.
	 * Doesn't update the current listener, since the current listener  is already the one
	 * that completed the drag operation.
	 *
	 * @param event the drag source event
	 * @see DragSourceListener#dragFinished(DragSourceEvent)
	 */
	@Override
	public void dragFinished(final DragSourceEvent event) {
		//		if (Policy.DEBUG_DRAG_DROP)
		//			System.out.println("Drag Finished: " + toString()); //$NON-NLS-1$
		SafeRunnable.run(new SafeRunnable() {
			@Override
			public void run() throws Exception {
				if (currentListener != null) {
					// there is a listener that can handle the drop, delegate the event
					currentListener.dragFinished(event);
				} else {
					// The drag was canceled and currentListener was never set, so send the
					// dragFinished event to all the active listeners.
					event.doit = false;
					Iterator<TransferDragSourceListener> iterator = activeListeners.iterator();
					while (iterator.hasNext()) {
						iterator.next()
								.dragFinished(event);
					}
				}
			}
		});
		currentListener = null;
		activeListeners.clear();
	}

	/**
	 * The drop data is requested.
	 * Updates the current listener and then forwards the event to it.
	 *
	 * @param event the drag source event
	 * @see DragSourceListener#dragSetData(DragSourceEvent)
	 */
	@Override
	public void dragSetData(final DragSourceEvent event) {
		//		if (Policy.DEBUG_DRAG_DROP)
		//			System.out.println("Drag Set Data: " + toString()); //$NON-NLS-1$

		updateCurrentListener(event); // find a listener that can provide the given data type
		if (currentListener != null) {
			SafeRunnable.run(new SafeRunnable() {
				@Override
				public void run() throws Exception {
					currentListener.dragSetData(event);
				}
			});
		}
	}

	/**
	 * A drag operation has started.
	 * Forwards this event to each listener. A listener must set <code>event.doit</code>
	 * to <code>false</code> if it cannot handle the drag operation. If a listener can
	 * handle the drag, it is added to the list of active listeners.
	 * The drag is aborted if there are no listeners that can handle it.
	 *
	 * @param event the drag source event
	 * @see DragSourceListener#dragStart(DragSourceEvent)
	 */
	@Override
	public void dragStart(final DragSourceEvent event) {
		//		if (Policy.DEBUG_DRAG_DROP)
		//			System.out.println("Drag Start: " + toString()); //$NON-NLS-1$
		boolean doit = false; // true if any one of the listeners can handle the drag
		List<Transfer> transfers = new ArrayList<>(listeners.size());

		activeListeners.clear();
		for (int i = 0; i < listeners.size(); i++) {
			final TransferDragSourceListener listener = listeners
					.get(i);
			event.doit = true; // restore event.doit
			SafeRunnable.run(new SafeRunnable() {
				@Override
				public void run() throws Exception {
					listener.dragStart(event);
				}
			});
			if (event.doit) { // the listener can handle this drag
				transfers.add(listener.getTransfer());
				activeListeners.add(listener);
			}
			doit |= event.doit;
		}

		if (doit) {
			((DragSource) event.widget).setTransfer(transfers
					.toArray(new Transfer[transfers.size()]));
		}

		event.doit = doit;
	}

	/**
	 * Returns the <code>Transfer</code>s from every
	 * <code>TransferDragSourceListener</code>.
	 *
	 * @return the combined <code>Transfer</code>s
	 */
	public Transfer[] getTransfers() {
		Transfer[] types = new Transfer[listeners.size()];
		for (int i = 0; i < listeners.size(); i++) {
			TransferDragSourceListener listener = listeners
					.get(i);
			types[i] = listener.getTransfer();
		}
		return types;
	}

	/**
	 * Returns <code>true</code> if there are no listeners to delegate drag events to.
	 *
	 * @return <code>true</code> if there are no <code>TransferDragSourceListeners</code>
	 * 	<code>false</code> otherwise.
	 */
	public boolean isEmpty() {
		return listeners.isEmpty();
	}

	/**
	 * Removes the given <code>TransferDragSourceListener</code>.
	 * Listeners should not be removed while a drag and drop operation is in progress.
	 *
	 * @param listener the <code>TransferDragSourceListener</code> to remove
	 */
	public void removeDragSourceListener(TransferDragSourceListener listener) {
		listeners.remove(listener);
		if (currentListener == listener) {
			currentListener = null;
		}
		if (activeListeners.contains(listener)) {
			activeListeners.remove(listener);
		}
	}

	/**
	 * Updates the current listener to one that can handle the drag. There can
	 * be many listeners and each listener may be able to handle many <code>TransferData</code>
	 * types.  The first listener found that supports one of the <code>TransferData</code>
	 * types specified in the <code>DragSourceEvent</code> will be selected.
	 *
	 * @param event the drag source event
	 */
	private void updateCurrentListener(DragSourceEvent event) {
		currentListener = null;
		if (event.dataType == null) {
			return;
		}
		Iterator<TransferDragSourceListener> iterator = activeListeners.iterator();
		while (iterator.hasNext()) {
			TransferDragSourceListener listener = iterator
					.next();

			if (listener.getTransfer().isSupportedType(event.dataType)) {
				//				if (Policy.DEBUG_DRAG_DROP)
				//					System.out.println("Current drag listener: " + listener); //$NON-NLS-1$
				currentListener = listener;
				return;
			}
		}
	}

}
