/*******************************************************************************
 * Copyright (c) 2006, 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
 *     Stephan Wahlbrink <stephan.wahlbrink@walware.de> - Wrong operations mode/feedback for text drag over/drop in text editors - https://bugs.eclipse.org/bugs/show_bug.cgi?id=206043
 *******************************************************************************/

package org.eclipse.ui.internal;

import java.util.ArrayList;
import java.util.List;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.DropTargetListener;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.dnd.TransferData;
import org.eclipse.swt.widgets.Control;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dnd.IDragAndDropService;
import org.eclipse.ui.services.IDisposable;

/**
 * <p>
 * Implementation for the <code>IDragAndDropService</code> to be used from
 * <code>EditorSite</code>'s.
 * </p>
 * <p>
 * Adds a drop target to the given control that merges the site's drop behaviour
 * with that specified by the <code>addMergedDropTarget</code> call.
 * </p>
 * <p>
 * The current implementation is only defined for EditorSite's and merges the
 * given drop handling with the existing EditorSashContainer's behaviour.
 * </p>
 * <p>
 * NOTE: There is no cleanup (i.e. 'dispose') handling necessary for merged Drop
 * Targets but the hooks are put into place to maintain the consistency of the
 * implementation pattern.
 * </p>
 *
 * @since 3.3
 *
 */
public class EditorSiteDragAndDropServiceImpl implements IDragAndDropService, IDisposable {
	// Key used to store/retrieve the MergedDropTarget instance from the real
	// DropTarget
	private static String MDT_KEY = "MDT"; //$NON-NLS-1$

	/**
	 * Implementation of a DropTarget wrapper that will either delegate to the
	 * <code>primaryListener</code> if the event's <code>currentDataType</code> can
	 * be handled by it; otherwise the event is forwarded on to the listener
	 * specified by <code>secondaryListener</code>.
	 * <p>
	 * NOTE: we should perhaps refactor this out into an external class
	 * </p>
	 *
	 * @since 3.3
	 *
	 */
	private static class MergedDropTarget {
		private DropTarget realDropTarget;

		private Transfer[] secondaryTransfers;
		private DropTargetListener secondaryListener;
		private int secondaryOps;

		private Transfer[] primaryTransfers;
		private DropTargetListener primaryListener;
		private int primaryOps;

		public MergedDropTarget(Control control, int priOps, Transfer[] priTransfers, DropTargetListener priListener,
				int secOps, Transfer[] secTransfers, DropTargetListener secListener) {
			realDropTarget = new DropTarget(control, priOps | secOps);
			realDropTarget.setData(MDT_KEY, this);

			// Cache the editor's transfers and listener
			primaryTransfers = priTransfers;
			primaryListener = priListener;
			primaryOps = priOps;

			// Cache the editor area's current transfers & listener
			secondaryTransfers = secTransfers;
			secondaryListener = secListener;
			secondaryOps = secOps;

			// Combine the two sets of transfers into one array
			Transfer[] allTransfers = new Transfer[secondaryTransfers.length + primaryTransfers.length];
			int curTransfer = 0;
			for (Transfer primaryTransfer : primaryTransfers) {
				allTransfers[curTransfer++] = primaryTransfer;
			}
			for (Transfer secondaryTransfer : secondaryTransfers) {
				allTransfers[curTransfer++] = secondaryTransfer;
			}
			realDropTarget.setTransfer(allTransfers);

			// Create a listener that will delegate to the appropriate listener
			// NOTE: the -editor- wins (i.e. it can over-ride WB behaviour if it wants
			realDropTarget.addDropListener(new DropTargetListener() {
				@Override
				public void dragEnter(DropTargetEvent event) {
					getAppropriateListener(event, true).dragEnter(event);
				}

				@Override
				public void dragLeave(DropTargetEvent event) {
					getAppropriateListener(event, false).dragLeave(event);
				}

				@Override
				public void dragOperationChanged(DropTargetEvent event) {
					getAppropriateListener(event, true).dragOperationChanged(event);
				}

				@Override
				public void dragOver(DropTargetEvent event) {
					getAppropriateListener(event, true).dragOver(event);
				}

				@Override
				public void drop(DropTargetEvent event) {
					getAppropriateListener(event, true).drop(event);
				}

				@Override
				public void dropAccept(DropTargetEvent event) {
					getAppropriateListener(event, true).dropAccept(event);
				}
			});
		}

		private DropTargetListener getAppropriateListener(DropTargetEvent event, boolean checkOperation) {
			if (isSupportedType(primaryTransfers, event.currentDataType)) {
				if (checkOperation && !isSupportedOperation(primaryOps, event.detail)) {
					event.detail = DND.DROP_NONE;
				}
				return primaryListener;
			}
			if (checkOperation && !isSupportedOperation(secondaryOps, event.detail)) {
				event.detail = DND.DROP_NONE;
			}
			return secondaryListener;
		}

		private boolean isSupportedType(Transfer[] transfers, TransferData transferType) {
			for (Transfer transfer : transfers) {
				if (transfer.isSupportedType(transferType))
					return true;
			}
			return false;
		}

		private boolean isSupportedOperation(int dropOps, int eventDetail) {
			return ((dropOps | DND.DROP_DEFAULT) & eventDetail) != 0;
		}
	}

	// Cache any listeners for cleanup
	List<Object> addedListeners = new ArrayList<>();

	@Override
	public void addMergedDropTarget(Control control, int ops, Transfer[] transfers, DropTargetListener listener) {
		// First we have to remove any existing drop target from the control
		removeMergedDropTarget(control);

		// Capture the editor area's current ops, transfers & listener
		int editorSiteOps = DND.DROP_DEFAULT | DND.DROP_COPY | DND.DROP_LINK;

		WorkbenchWindow ww = (WorkbenchWindow) PlatformUI.getWorkbench().getActiveWorkbenchWindow();
		WorkbenchWindowConfigurer winConfigurer = ww.getWindowConfigurer();
		Transfer[] editorSiteTransfers = winConfigurer.getTransfers();
		DropTargetListener editorSiteListener = winConfigurer.getDropTargetListener();

		// Create a new 'merged' drop Listener using combination of the desired
		// transfers and the ones used by the EditorArea
		MergedDropTarget newTarget = new MergedDropTarget(control, ops, transfers, listener, editorSiteOps,
				editorSiteTransfers, editorSiteListener);
		addedListeners.add(newTarget);

		newTarget.realDropTarget.addDisposeListener(e -> {
			Object mdt = e.widget.getData(MDT_KEY);
			addedListeners.remove(mdt);
		});
	}

	/**
	 * This method will return the current drop target for the control (whether or
	 * not it was created using this service.
	 *
	 * @param control The control to get the drop target for
	 * @return The DropTarget for that control (could be null
	 */
	private DropTarget getCurrentDropTarget(Control control) {
		if (control == null)
			return null;

		Object curDT = control.getData(DND.DROP_TARGET_KEY);
		return (DropTarget) curDT;
	}

	@Override
	public void removeMergedDropTarget(Control control) {
		DropTarget targetForControl = getCurrentDropTarget(control);
		if (targetForControl != null) {
			targetForControl.dispose();
			addedListeners.remove(targetForControl);
		}
	}

	@Override
	public void dispose() {
		addedListeners.clear();
	}

}
