/*******************************************************************************
 * Copyright (c) 2010, 2018 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
 *     Christoph Läubrich - Bug 365525
 ******************************************************************************/
package org.eclipse.e4.ui.workbench.addons.dndaddon;

import java.util.List;
import org.eclipse.e4.ui.model.application.ui.MUIElement;
import org.eclipse.e4.ui.model.application.ui.advanced.MArea;
import org.eclipse.e4.ui.model.application.ui.advanced.MPerspective;
import org.eclipse.e4.ui.model.application.ui.basic.MPartSashContainerElement;
import org.eclipse.e4.ui.model.application.ui.basic.MPartStack;
import org.eclipse.e4.ui.model.application.ui.basic.MStackElement;
import org.eclipse.e4.ui.model.application.ui.basic.MWindow;
import org.eclipse.e4.ui.model.application.ui.basic.impl.BasicFactoryImpl;
import org.eclipse.e4.ui.workbench.IPresentationEngine;
import org.eclipse.e4.ui.workbench.modeling.EModelService;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

/**
 *
 */
public class SplitDropAgent2 extends DropAgent {
	private static final int TOLERANCE = 35;

	private MUIElement relToElement = null;

	private SplitFeedbackOverlay feedback = null;

	private int dragElementLocation;

	private int where;

	private float ratio = 0.5f;

	private Rectangle trackRect;

	private boolean wasModified = false;

	/**
	 * @param manager
	 *            the DnDManager using this agent
	 */
	public SplitDropAgent2(DnDManager manager) {
		super(manager);
	}

	@Override
	public boolean canDrop(MUIElement dragElement, DnDInfo info) {
		if (!(dragElement instanceof MStackElement) && !(dragElement instanceof MPartStack)) {
			return false;
		}

		relToElement = getTargetElement(dragElement, info);
		return relToElement != null;
	}

	private MUIElement getTargetElement(MUIElement dragElement, DnDInfo info) {
		MUIElement target = null;

		dragElementLocation = dndManager.getModelService().getElementLocation(dragElement);

		// If we're inside a perspective somewhere check the shared area's edges
		if (dragElementLocation == EModelService.IN_SHARED_AREA
				|| dragElementLocation == EModelService.IN_ACTIVE_PERSPECTIVE) {
			target = checkAreaEdge(dragElement, info);
		}

		if (target == null && dragElementLocation == EModelService.IN_ACTIVE_PERSPECTIVE
				|| dragElementLocation == EModelService.OUTSIDE_PERSPECTIVE) {
			target = checkPerspectiveEdge(dragElement, info);
		}

		if (target == null) {
			target = checkStacks(dragElement, info);
		}

		return target;
	}

	private boolean isInCursorShell(DnDInfo info, Control ctrl) {
		if (ctrl == null || info.curCtrl == null || info.curCtrl.isDisposed()) {
			return false;
		}
		Shell infoShell = (Shell) (info.curCtrl instanceof Shell ? info.curCtrl : info.curCtrl
				.getShell());
		return ctrl.getShell() == infoShell;
	}

	private MPartStack checkStacks(final MUIElement dragElement, final DnDInfo info) {
		MPartStack candidateStack = null;

		// Collect up the elements we can be split 'relative to'
		final EModelService ms = dndManager.getModelService();
		List<MPartStack> stacks = ms.findElements(ms.getTopLevelWindowFor(dragElement),
				MPartStack.class, EModelService.PRESENTATION, element -> {
					MPartStack stack = (MPartStack) element;

					// Has to be visible...
					if (!stack.isVisible() || !(stack.getWidget() instanceof CTabFolder)) {
						return false;
					}

					// ...not disposed...
					CTabFolder ctf = (CTabFolder) stack.getWidget();
					if (ctf.isDisposed()) {
						return false;
					}

					// ...and in the shell the cursor is over
					if (!isInCursorShell(info, ctf)) {
						return false;
					}

					// ...and the cursor must be in the CTF's client area
					Rectangle bb = ctf.getClientArea();
					bb = ctf.getDisplay().map(ctf, null, bb);
					if (!bb.contains(info.cursorPos)) {
						return false;
					}

					// Can't split with ourselves if we're dragging a stack
					if (dragElement instanceof MPartStack && stack == dragElement) {
						return false;
					}

					// Can't split with ourselves if we're dragging the only visible element in
					// a stack
					MUIElement deParent = dragElement.getParent();
					if (dragElement instanceof MStackElement && stack == deParent
							&& ms.countRenderableChildren(deParent) == 1) {
						return false;
					}

					return true;
				});

		if (stacks.size() > 0) {
			candidateStack = stacks.get(0);
			if (candidateStack.getWidget() instanceof CTabFolder) {
				CTabFolder ctf = (CTabFolder) (candidateStack.getWidget());
				trackRect = ctf.getClientArea();
				trackRect = ctf.getDisplay().map(ctf, null, trackRect);
			}
		}

		return candidateStack;
	}

	private MUIElement checkAreaEdge(MUIElement dragElement, DnDInfo info) {
		MPerspective persp = dndManager.getModelService().getPerspectiveFor(dragElement);
		if (persp == null) {
			return null;
		}
		List<MArea> areaList = dndManager.getModelService().findElements(persp, null, MArea.class,
				null, EModelService.IN_SHARED_AREA);
		if (areaList.size() > 0) {
			MArea area = areaList.get(0);
			Control ctrl = (Control) area.getWidget();
			if (checkEdge(info, ctrl)) {
				return area;
			}
		}

		return null;
	}

	private MUIElement checkPerspectiveEdge(MUIElement dragElement, DnDInfo info) {
		MWindow win = dndManager.getModelService().getTopLevelWindowFor(dragElement);
		MPerspective persp = dndManager.getModelService().getActivePerspective(win);
		if (persp != null && persp.getWidget() instanceof Control) {
			Control ctrl = (Control) persp.getWidget();
			if (checkEdge(info, ctrl)) {
				return persp;
			}
		}

		return null;
	}

	private boolean checkEdge(DnDInfo info, Control ctrl) {
		boolean onEdge = false;

		if (!isInCursorShell(info, ctrl)) {
			return false;
		}

		Rectangle bb = ctrl.getBounds();
		bb = ctrl.getDisplay().map(ctrl.getParent(), null, bb);
		if (bb.contains(info.cursorPos)) {
			Point p = info.cursorPos;
			if (p.x - bb.x < TOLERANCE) {
				where = EModelService.LEFT_OF;
				trackRect = new Rectangle(bb.x, bb.y, TOLERANCE, bb.height);
				onEdge = true;
				// } else if (p.y - bb.y < TOLERANCE) {
				// where = EModelService.ABOVE;
				// trackRect = new Rectangle(bb.x, bb.y, bb.width, TOLERANCE);
				// onEdge = true;
			} else if ((bb.x + bb.width) - p.x < TOLERANCE) {
				where = EModelService.RIGHT_OF;
				trackRect = new Rectangle(bb.x + (bb.width - TOLERANCE), bb.y, TOLERANCE,
						bb.height);
				onEdge = true;
			} else if ((bb.y + bb.height) - p.y < TOLERANCE) {
				where = EModelService.BELOW;
				trackRect = new Rectangle(bb.x, bb.y + (bb.height - TOLERANCE), bb.width,
						TOLERANCE);
				onEdge = true;
			}
		}
		return onEdge;
	}

	@Override
	public void dragEnter(final MUIElement dragElement, DnDInfo info) {
		super.dragEnter(dragElement, info);

		where = setRelToInfo(info);

		showFeedback();
	}

	private int setRelToInfo(DnDInfo info) {
		if (relToElement instanceof MPerspective || relToElement instanceof MArea) {
			ratio = 0.33f; // 'where' has already been set
			return where;
		} else if (relToElement instanceof MPartStack) {
			ratio = 0.5f;
			Point p = info.cursorPos;
			int dTop = p.y - trackRect.y;
			int dBottom = (trackRect.y + trackRect.height) - p.y;
			int dLeft = p.x - trackRect.x;
			int dRight = (trackRect.x + trackRect.width) - p.x;

			if (trackRect.width >= trackRect.height) {
				int thirdOfWidth = trackRect.width / 3;
				// System.out.println("tow: " + thirdOfWidth + " dLeft: " + dLeft + " dRight: "
				// + dRight);
				if (dLeft < thirdOfWidth) {
					return EModelService.LEFT_OF;
				} else if (dRight < thirdOfWidth) {
					return EModelService.RIGHT_OF;
				} else {
					return dTop < dBottom ? EModelService.ABOVE : EModelService.BELOW;
				}
			}

			int thirdOfHeight = trackRect.height / 3;
			if (dTop < thirdOfHeight) {
				return EModelService.ABOVE;
			} else if (dBottom < thirdOfHeight) {
				return EModelService.BELOW;
			} else {
				return dLeft < dRight ? EModelService.LEFT_OF : EModelService.RIGHT_OF;
			}
		}

		return -1;
	}

	@Override
	public void dragLeave(MUIElement dragElement, DnDInfo info) {
		dndManager.clearOverlay();
		clearFeedback();
		relToElement = null;

		reactivatePart(dragElement);

		super.dragLeave(dragElement, info);
	}

	@Override
	public boolean drop(MUIElement dragElement, DnDInfo info) {
		MPartSashContainerElement toInsert = (MPartSashContainerElement) dragElement;
		if (dragElement instanceof MPartStack) {
			// Ensure we restore the stack to the presentation first
			if (toInsert.getTags().contains(IPresentationEngine.MINIMIZED)) {
				toInsert.getTags().remove(IPresentationEngine.MINIMIZED);
			}

			toInsert.getParent().getChildren().remove(toInsert);
		} else {
			// wrap it in a stack if it's a part
			MStackElement stackElement = (MStackElement) dragElement;
			MPartStack newStack = BasicFactoryImpl.eINSTANCE.createPartStack();
			newStack.getChildren().add(stackElement);
			newStack.setSelectedElement(stackElement);
			toInsert = newStack;
		}

		// treat the lone editor area stack as if it were the area
		if (dndManager.getModelService().isLastEditorStack(relToElement)) {
			MUIElement targetParent = relToElement.getParent();
			while (!(targetParent instanceof MArea)) {
				targetParent = targetParent.getParent();
			}
			relToElement = targetParent;
		}

		// Adjust the relToElement based on the location of the dragElement
		if (relToElement instanceof MArea) {
			// make it difficult to drag outside parts into the shared area
			boolean fromSharedArea = dragElementLocation == EModelService.IN_SHARED_AREA;
			// if from shared area and no modifier, is ok
			// if not from shared area and modifier is on, then ok
			boolean shouldBePlacedInSharedArea = fromSharedArea == !isModified();
			if (shouldBePlacedInSharedArea) {
				MArea area = (MArea) relToElement;
				relToElement = area.getChildren().get(0);
			}
		} else if (relToElement instanceof MPerspective) {
			if (dragElementLocation == EModelService.IN_ACTIVE_PERSPECTIVE) {
				MPerspective persp = (MPerspective) relToElement;
				relToElement = persp.getChildren().get(0);
			}
		}

		dndManager.getModelService().insert(toInsert, (MPartSashContainerElement) relToElement,
				where, ratio);
		// reactivatePart(dragElement);

		return true;
	}

	private boolean isModified() {
		return dndManager.isModified
				&& (relToElement instanceof MArea || relToElement instanceof MPerspective || dndManager
						.getModelService().isLastEditorStack(relToElement));
	}

	@Override
	public boolean track(MUIElement dragElement, DnDInfo info) {
		if (getTargetElement(dragElement, info) != relToElement) {
			dndManager.setCursor(Display.getCurrent().getSystemCursor(SWT.CURSOR_NO));
			return false;
		}

		if (!trackRect.contains(info.cursorPos)) {
			dndManager.setCursor(Display.getCurrent().getSystemCursor(SWT.CURSOR_NO));
			return false;
		}

		dndManager.setCursor(Display.getCurrent().getSystemCursor(SWT.CURSOR_HAND));

		int curWhere = where;
		where = setRelToInfo(info);

		if (where == curWhere && wasModified == isModified()) {
			return true;
		}
		wasModified = isModified();

		showFeedback();

		return true;
	}

	private void showFeedback() {
		if (relToElement == null || !(relToElement.getWidget() instanceof Control)) {
			return;
		}

		int side = -1;
		if (where == EModelService.ABOVE) {
			side = SWT.TOP;
		} else if (where == EModelService.BELOW) {
			side = SWT.BOTTOM;
		} else if (where == EModelService.LEFT_OF) {
			side = SWT.LEFT;
		} else if (where == EModelService.RIGHT_OF) {
			side = SWT.RIGHT;
		}

		if (feedback != null) {
			feedback.dispose();
		}

		Control ctrl = (Control) relToElement.getWidget();
		Rectangle bb = ctrl.getBounds();
		bb = ctrl.getDisplay().map(ctrl.getParent(), null, bb);
		feedback = new SplitFeedbackOverlay(ctrl.getShell(), bb, side, ratio, isModified(),
				isModified());
		feedback.setVisible(true);
	}

	private void clearFeedback() {
		if (feedback == null) {
			return;
		}

		feedback.dispose();
		feedback = null;
	}
}
