package org.eclipse.mylyn.internal.context.ui;

import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.mylyn.commons.ui.CommonImages;
import org.eclipse.mylyn.commons.ui.PlatformUiUtil;
import org.eclipse.mylyn.commons.workbench.WorkbenchUtil;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.ToolTip;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.PlatformUI;

/**
 * @author Mik Kersten
 */

public class FilteredChildrenDecorationDrawer implements Listener {

	private final class MoveListener implements MouseMoveListener, MouseListener, MouseTrackListener {

		private TreeItem lastItem;

		private TreeItem currentItem;

		private long lastMoveTime;

		private long startMoveTime;

		private final TreeViewer viewer;

		private final BrowseFilteredListener browseFilteredListener;

		private ToolTip toolTip;

		public MoveListener(TreeViewer viewer, BrowseFilteredListener browseFilteredListener) {
			this.viewer = viewer;
			this.browseFilteredListener = browseFilteredListener;
		}

		public void mouseEnter(MouseEvent e) {
			mouseMove(e);
		}

		public void mouseExit(MouseEvent e) {
			if (lastItem != null && !lastItem.isDisposed()) {
				lastItem.setData(ID_HOVER, NodeState.LESS);
			}
			if (toolTip != null && !toolTip.isDisposed()) {
				toolTip.setVisible(false);
				toolTip.dispose();
				toolTip = null;
			}

			redrawTree(lastItem);
			lastItem = null;
			currentItem = null;
		}

		private void redrawTree(TreeItem item) {
			if (viewer.getTree() != null && !viewer.getTree().isDisposed()) {
				if (item != null && !item.isDisposed()) {
					Rectangle bounds = item.getBounds();
					Rectangle treeBounds = viewer.getTree().getBounds();
					viewer.getTree().redraw(0, bounds.y, treeBounds.width, bounds.height, true);
				} else {
					viewer.getTree().redraw();
				}
			}
		}

		public void mouseHover(MouseEvent e) {

			if (toolTip == null || toolTip.isDisposed()) {
				toolTip = new ToolTip(WorkbenchUtil.getShell(), PlatformUiUtil.getSwtTooltipStyle());
			}

			if (toolTip != null && !toolTip.isDisposed()) {
				Tree tree = (Tree) e.widget;
				TreeItem item = findItem(tree, e.y);
				if (item != null && !item.isDisposed()) {
					Object data = item.getData(ID_HOVER);
					if (data == NodeState.MORE) {
						toolTip.setMessage(Messages.FilteredChildrenDecorationDrawer_Show_Filtered_Children);
					} else {
						toolTip.setMessage(Messages.FilteredChildrenDecorationDrawer_No_Filtered_Children);
					}
					if (inImageBounds(tree, item, e)) {
						toolTip.setVisible(true);
					}
				}
			}
		}

		private boolean inImageBounds(Tree tree, TreeItem item, MouseEvent e) {
			int selectedX = e.x;

			int imageStartX = getImageStartX(item.getBounds().x, item.getBounds().width, tree);

			int imageEndX = imageStartX + moreImage.getBounds().width;

			return selectedX > imageStartX && selectedX < imageEndX;
		}

		public void mouseMove(MouseEvent e) {
			if (toolTip != null && !toolTip.isDisposed()) {
				toolTip.setVisible(false);
				toolTip.dispose();
				toolTip = null;
			}

			if (!(e.widget instanceof Tree) || e.widget.isDisposed()) {
				return;
			}
			Tree tree = (Tree) e.widget;
			final TreeItem item = findItem(tree, e.y);
			if (item != null && !item.isDisposed()) {
				if (lastItem != null && !lastItem.isDisposed() && !lastItem.equals(item)) {
					boolean redraw = lastItem.getData(ID_HOVER) != NodeState.LESS;
					lastItem.setData(ID_HOVER, NodeState.LESS);
					if (redraw) {// hide the + immediately
						redrawTree(lastItem);
					}
				}

				currentItem = item;
				long currentTime = System.currentTimeMillis();
				if (currentTime - lastMoveTime > 250) {// user paused movement
					startMoveTime = currentTime;
				}

				lastMoveTime = currentTime;
				// be responsive for small moves but delay more for bigger ones
				int delay = Math.min(100, (int) ((currentTime - startMoveTime) / 4.0));
				PlatformUI.getWorkbench().getDisplay().timerExec(delay, new Runnable() {
					public void run() {// do nothing if we aren't using the most recent item
						if (currentItem == item && !item.isDisposed()) {
							if (item.getData(ID_HOVER) != NodeState.MORE_ERROR) {
								item.setData(ID_HOVER, NodeState.MORE);
							}
							if (lastItem == null || (!lastItem.isDisposed() && !lastItem.equals(item))) {
								redrawTree(lastItem);
								redrawTree(item);
							}
							lastItem = item;
						}
					}
				});
			} else {
				if (lastItem != null && !lastItem.isDisposed() && !lastItem.equals(item)) {
					lastItem.setData(ID_HOVER, NodeState.LESS);
					redrawTree(lastItem);
				}
				lastItem = item;
			}
		}

		public void dispose() {
			if (toolTip != null && !toolTip.isDisposed()) {
				toolTip.dispose();
			}
		}

		public void mouseDoubleClick(MouseEvent e) {
			// ignore

		}

		public void mouseDown(MouseEvent e) {
			if (toolTip != null && !toolTip.isDisposed()) {
				toolTip.setVisible(false);
				toolTip.dispose();
				toolTip = null;
			}

			if (!(e.widget instanceof Tree) || e.widget.isDisposed()) {
				// we only handle tree's
				return;
			}

			Tree tree = (Tree) e.widget;
			TreeItem item = findItem(tree, e.y);

			if (item == null || item.isDisposed() || e.button != 1) {
				// we can't do anything if we cant find the tree items
				return;
			}

			int prevNumberItems = item.getItemCount();
			boolean prevHasData = true;
			if (prevNumberItems > 0) {
				prevHasData = item.getItem(0).getData() != null;
			}
			if (inImageBounds(tree, item, e)) {
				browseFilteredListener.setWasExternalClick(true);
				browseFilteredListener.unfilterSelection(viewer, new StructuredSelection(item.getData()));

				int newNumItems = item.getItemCount();
				if (newNumItems == prevNumberItems && prevHasData) {
					item.setData(ID_HOVER, NodeState.MORE_ERROR);
					redrawTree(item);
				}
			}
		}

		public void mouseUp(MouseEvent e) {
			// ignore

		}
	}

	private static final int IMAGE_PADDING = 5;

	// XXX Update Images
	private final Image moreImage = CommonImages.getImage(CommonImages.EXPAND);

	private final Image moreErrorImage = CommonImages.getImage(CommonImages.REMOVE);

	enum NodeState {
		MORE, LESS, MORE_ERROR
	};

	private static final String ID_HOVER = "mylyn-context-hover"; //$NON-NLS-1$

	private final TreeViewer treeViewer;

	private MoveListener listener;

	private final BrowseFilteredListener browseFilteredListener;

	public FilteredChildrenDecorationDrawer(TreeViewer treeViewer, BrowseFilteredListener browseFilteredListener) {
		this.treeViewer = treeViewer;
		this.browseFilteredListener = browseFilteredListener;
	}

	public boolean applyToTreeViewer() {
		Object data = treeViewer.getData(FilteredChildrenDecorationDrawer.class.getName());
		if (data == null || Boolean.FALSE.equals(data)) {
			treeViewer.setData(FilteredChildrenDecorationDrawer.class.getName(), Boolean.TRUE);
			if (treeViewer.getTree() != null && !treeViewer.getTree().isDisposed()) {
				treeViewer.getTree().addListener(SWT.PaintItem, this);

				listener = new MoveListener(treeViewer, browseFilteredListener);
				treeViewer.getTree().addMouseMoveListener(listener);
				treeViewer.getTree().addMouseListener(listener);
				treeViewer.getTree().addMouseTrackListener(listener);
				return true;
			}
		}
		return false;
	}

	public void dispose() {
		if (treeViewer.getTree() == null || treeViewer.getTree().isDisposed()) {
			return;
		}
		treeViewer.setData(FilteredChildrenDecorationDrawer.class.getName(), Boolean.FALSE);
		treeViewer.getTree().removeListener(SWT.PaintItem, this);

		if (listener != null) {
			treeViewer.getTree().removeMouseMoveListener(listener);
			treeViewer.getTree().removeMouseListener(listener);
			treeViewer.getTree().removeMouseTrackListener(listener);
			listener.dispose();
		}
	}

	/*
	 * NOTE: MeasureItem, PaintItem and EraseItem are called repeatedly.
	 * Therefore, it is critical for performance that these methods be as
	 * efficient as possible.
	 */
	public void handleEvent(Event event) {

		if (!(event.widget instanceof Tree)) {
			// we only handle tree's
			return;
		}

		switch (event.type) {
		case SWT.PaintItem: {
			Tree tree = (Tree) event.widget;
			if (tree.isDisposed() || event.index != 0) {
				return;
			}
			TreeItem item = findItem(tree, event.y);
			if (item == null || item.isDisposed()) {
				return;
			}

			int imageStartX = getImageStartX(event.x, event.width, tree);

			NodeState value = (NodeState) item.getData(ID_HOVER);

			int imageStartY = event.y;
			int imageHeight = moreImage.getBounds().height;
			if (value != null && value.equals(NodeState.MORE_ERROR)) {
				imageHeight = moreErrorImage.getBounds().height;
			}

			int offset = Math.round(((float) event.height) / 2 - ((float) imageHeight) / 2);
			imageStartY += offset;
			Rectangle clipping = event.gc.getClipping();
			if (clipping.width < imageStartX && clipping.width > 0) {
				clipping.width += IMAGE_PADDING + moreImage.getBounds().width;
				event.gc.setClipping(clipping);
			}
			if (value != null && value.equals(NodeState.MORE)) {
				event.gc.drawImage(moreImage, imageStartX, imageStartY);
			} else if (value != null && value.equals(NodeState.MORE_ERROR)) {
				event.gc.drawImage(moreErrorImage, imageStartX, imageStartY);
			}
			break;
		}
		}
	}

	private int getImageStartX(int x, int width, Tree tree) {
		int imageStartX = x + width + IMAGE_PADDING;

		int imageEndX = imageStartX + moreImage.getBounds().width;

		Rectangle clientArea = tree.getClientArea();
		int currentTreeBounds = clientArea.x + clientArea.width;
		if (imageStartX > currentTreeBounds) {
			imageStartX = currentTreeBounds - moreImage.getBounds().width;
		}

		float tolerance = 0;//moreImage.getBounds().width - (((float) moreImage.getBounds().width) / 3 * 2); // draw over item if more than 33% of the image is hidden 
		if (imageEndX > currentTreeBounds && imageEndX - currentTreeBounds > tolerance) {
			imageStartX = currentTreeBounds - moreImage.getBounds().width;
		}
		return imageStartX;
	}

	private TreeItem findItem(Tree tree, int y) {
		TreeItem item = null;
		Point size = tree.getSize();
		final int RATE = 17;
		for (int i = 0; i <= RATE && item == null; i++) {
			int position = size.x / RATE + (i * size.x / RATE);
			item = tree.getItem(new Point(position, y));
		}
		return item;
	}
}
