/*******************************************************************************
 * Copyright (c) 2008, 2011 Andrew Gvozdev.
 *
 * 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:
 *    Andrew Gvozdev (Quoin Inc.) - Initial implementation
 *******************************************************************************/

package org.eclipse.cdt.make.internal.ui.dnd;

import org.eclipse.cdt.make.core.IMakeTarget;
import org.eclipse.cdt.make.ui.TargetSourceContainer;
import org.eclipse.core.resources.IContainer;
import org.eclipse.jface.util.TransferDropTargetListener;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.DropTargetListener;
import org.eclipse.swt.dnd.TransferData;

/**
 * This abstract class provides a frame for {@link DropTargetListener} able to accept a
 * drop into a container area. A container area includes the container itself and
 * the items directly belonging to the container. A drop into the container area is
 * treated as a drop on the container itself. Also, some flexibility for adjusting
 * drop operation is provided.
 *
 * @see org.eclipse.swt.dnd.DropTargetListener
 *
 */
public abstract class AbstractContainerAreaDropAdapter implements TransferDropTargetListener {

	private int originallyRequestedOperation = DND.DROP_NONE;
	private Object lastDragOverTarget = null;
	private int lastDragOverOperation = DND.DROP_NONE;

	/**
	 * This method lets changing initial drag operation. The operation will be
	 * indicated by mouse cursor. This operation is passed in {@code event.detail}
	 * to {@code dragEnter} method.
	 *
	 * @param operation - incoming operation.
	 * @return changed operation. The return must be one of
	 *         {@link org.eclipse.swt.dnd.DND} operations.
	 *
	 * @see DropTargetListener#dragEnter(DropTargetEvent)
	 * @see DND#DROP_NONE
	 * @see DND#DROP_COPY
	 * @see DND#DROP_MOVE
	 * @see DND#DROP_LINK
	 * @see DND#DROP_DEFAULT
	 */
	protected abstract int dragEnterOperation(int operation);

	/**
	 * This method lets adjust drag operation over container area. The operation
	 * will be indicated by mouse cursor. This operation is passed in {@code
	 * event.detail} to {@code dragOver} method.
	 *
	 * @param operation - incoming operation.
	 * @param dropContainer - container where drop is going to be.
	 * @param dropTarget - drop target.
	 * @return changed operation. The return must be one of
	 *         {@link org.eclipse.swt.dnd.DND} operations.
	 *
	 *
	 * @see DropTargetListener#dragOver(DropTargetEvent)
	 * @see DND#DROP_NONE
	 * @see DND#DROP_COPY
	 * @see DND#DROP_MOVE
	 * @see DND#DROP_LINK
	 * @see DND#DROP_DEFAULT
	 */
	protected abstract int dragOverOperation(int operation, IContainer dropContainer, Object dropTarget);

	/**
	 * Implementation of the actual drop of {@code dropObject} to {@code dropContainer}.
	 *
	 * @param dropObject - object to drop.
	 * @param dropContainer - container where to drop the object.
	 * @param operation - drop operation.
	 *
	 * @see DropTargetListener#drop(DropTargetEvent)
	 */
	protected abstract void dropToContainer(Object dropObject, IContainer dropContainer, int operation);

	/**
	 * Defines if a transfer data is supported by the implementer.
	 *
	 * @see org.eclipse.swt.dnd.Transfer#isSupportedType
	 *
	 * @param transferData - data type to examine
	 * @return whether the given data type is supported by this listener
	 */
	public boolean isSupportedType(TransferData transferData) {
		return getTransfer().isSupportedType(transferData);
	}

	/**
	 * This function is called only the listener is able to handle provided data type.
	 * By default {@code isEnabled} returns {@code true} but subclasses are free to
	 * override.
	 *
	 * @param event - the information associated with the drag event.
	 * @return {@code true}
	 *
	 * @see DropTargetEvent
	 */
	@Override
	public boolean isEnabled(DropTargetEvent event) {
		return true;
	}

	/**
	 * Implementation of {@code DropTargetListener#dragEnter} to support
	 * dropping into container area.
	 *
	 * @param event - the information associated with the drag event
	 *
	 * @see DropTargetEvent
	 */
	@Override
	public void dragEnter(DropTargetEvent event) {
		lastDragOverTarget = null;
		lastDragOverOperation = DND.DROP_NONE;

		if (isSupportedType(event.currentDataType)) {
			originallyRequestedOperation = event.detail;
			event.detail = dragEnterOperation(originallyRequestedOperation);
		} else {
			event.detail = DND.DROP_NONE;
			originallyRequestedOperation = DND.DROP_NONE;
		}
	}

	/**
	 * Implementation of {@code DropTargetListener#dragOperationChanged} to support
	 * dropping into container area.
	 *
	 * @param event - the information associated with the drag event
	 *
	 * @see DropTargetEvent
	 */
	@Override
	public void dragOperationChanged(DropTargetEvent event) {
		originallyRequestedOperation = event.detail;
		event.detail = dragOverOperationCached(originallyRequestedOperation, determineDropContainer(event),
				determineDropTarget(event));
	}

	/**
	 * Implementation of {@code DropTargetListener#dragOver} to support
	 * dropping into container area.
	 *
	 * @param event - the information associated with the drag event
	 *
	 * @see DropTargetEvent
	 */
	@Override
	public void dragOver(DropTargetEvent event) {
		event.detail = dragOverOperationCached(originallyRequestedOperation, determineDropContainer(event),
				determineDropTarget(event));

		if (originallyRequestedOperation != DND.DROP_NONE) {
			// let user discover items even if event.detail is DND.DROP_NONE
			// since the original operation could be applicable to other items
			event.feedback = DND.FEEDBACK_SCROLL | DND.FEEDBACK_EXPAND;
			if (event.detail != DND.DROP_NONE) {
				event.feedback = event.feedback | DND.FEEDBACK_SELECT;
			}
		} else {
			event.feedback = DND.FEEDBACK_NONE;
		}
	}

	/**
	 * Implementation of {@code DropTargetListener#dragLeave} to support
	 * dropping into container area.
	 *
	 * @param event - the information associated with the drag event
	 *
	 * @see DropTargetEvent
	 */
	@Override
	public void dragLeave(DropTargetEvent event) {
		// no action
	}

	/**
	 * Implementation of {@code DropTargetListener#dropAccept} to support
	 * dropping into container area.
	 *
	 * @param event - the information associated with the drag event
	 *
	 * @see DropTargetEvent
	 */
	@Override
	public void dropAccept(DropTargetEvent event) {
		// no action
	}

	/**
	 * Implementation of {@code DropTargetListener#drop} to support
	 * dropping into container area.
	 *
	 * @param event - the information associated with the drag event
	 *
	 * @see DropTargetEvent
	 */
	@Override
	public void drop(DropTargetEvent event) {
		IContainer dropContainer = determineDropContainer(event);
		if (dropContainer != null) {
			event.detail = dragOverOperationCached(event.detail, dropContainer, determineDropTarget(event));
			dropToContainer(event.data, dropContainer, event.detail);
		} else {
			event.detail = DND.DROP_NONE;
		}
	}

	/**
	 * This method provides caching of potentially long running and called on each
	 * mouse move {@link #dragOverOperation}.
	 *
	 * @param operation - incoming operation.
	 * @param dropContainer - container where drop is going to be.
	 * @param dropTarget - drop target.
	 * @return changed operation. The return must be one of
	 *         org.eclipse.swt.dnd.DND operations such as {@link DND#DROP_NONE},
	 *         {@link DND#DROP_COPY}, {@link DND#DROP_MOVE},
	 *         {@link DND#DROP_LINK}
	 *
	 *
	 * @see DropTargetListener#dragOver(DropTargetEvent)
	 */
	private int dragOverOperationCached(int operation, IContainer dropContainer, Object dropTarget) {
		if (dropTarget != lastDragOverTarget || operation != lastDragOverOperation) {
			lastDragOverOperation = dragOverOperation(operation, dropContainer, dropTarget);
			lastDragOverTarget = dropTarget;
		}
		return lastDragOverOperation;
	}

	/**
	 * Returns the drop target passed by {@code event.item}.
	 *
	 * @param event - the information associated with the drop event
	 */
	private static Object determineDropTarget(DropTargetEvent event) {
		return event.item == null ? null : event.item.getData();
	}

	/**
	 * Find which container area the mouse cursor is currently in.
	 *
	 * @param event - the information associated with the drop event
	 * @return the container of the current mouse location.
	 */
	public static IContainer determineDropContainer(DropTargetEvent event) {
		Object dropTarget = determineDropTarget(event);
		if (dropTarget instanceof IMakeTarget) {
			return ((IMakeTarget) dropTarget).getContainer();
		} else if (dropTarget instanceof IContainer) {
			return (IContainer) dropTarget;
		} else if (dropTarget instanceof TargetSourceContainer) {
			IContainer dropContainer = ((TargetSourceContainer) dropTarget).getContainer();
			return dropContainer;
		}
		return null;
	}

}
