/*******************************************************************************
 * Copyright (c) 2006, 2007 Wind River Systems, Inc. and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Anton Leherbauer (Wind River Systems) - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.dnd;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DefaultPositionUpdater;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.Position;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.StyledTextContent;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSourceAdapter;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.ui.texteditor.ITextEditorExtension;

/**
 * Drag source adapter for text selections in ITextViewers.
 * 
 * @since 4.0
 */
public class TextViewerDragAdapter extends DragSourceAdapter {

	/** The position category to be used to indicate a drag source selection */
	public final static String DRAG_SELECTION_CATEGORY= "dragSelectionCategory"; //$NON-NLS-1$
	/** The position updater for the drag selection position */
	private IPositionUpdater fPositionUpdater;
	/** The drag selection position */
	private Position fSelectionPosition;
	/** The text viewer allowing drag */
	private ITextViewer fViewer;
	/** The editor of the viewer (may be null) */
	private ITextEditorExtension fEditor;
	/** Flag whether this drag source listener allows to drag */
	private boolean fIsEnabled= true;

	/**
	 * Create a new TextViewerDragAdapter.
	 * @param viewer the text viewer
	 */
	public TextViewerDragAdapter(ITextViewer viewer) {
		this(viewer, null);
	}

	/**
	 * Create a new TextViewerDragAdapter.
	 * @param viewer the text viewer
	 */
	public TextViewerDragAdapter(ITextViewer viewer, ITextEditorExtension editor) {
		fViewer= viewer;
		fEditor= editor;
	}

	/*
	 * @see org.eclipse.swt.dnd.DragSourceListener#dragFinished(org.eclipse.swt.dnd.DragSourceEvent)
	 */
	@Override
	public void dragFinished(DragSourceEvent event) {
		IDocument doc= fViewer.getDocument();
		try {
			doc.removePositionCategory(DRAG_SELECTION_CATEGORY);
			doc.removePositionUpdater(fPositionUpdater);
		} catch (BadPositionCategoryException e1) {
			// cannot happen
		}
		if (event.doit && event.detail == DND.DROP_MOVE && isDocumentEditable()) {
			try {
				doc.replace(fSelectionPosition.offset, fSelectionPosition.length, null);
			} catch (BadLocationException e) {
				// ignore
			}
		}
		if (fViewer instanceof ITextViewerExtension) {
			((ITextViewerExtension)fViewer).getRewriteTarget().endCompoundChange();
		}
	}

	/*
	 * @see org.eclipse.swt.dnd.DragSourceListener#dragSetData(org.eclipse.swt.dnd.DragSourceEvent)
	 */
	@Override
	public void dragSetData(DragSourceEvent event) {
		IDocument doc= fViewer.getDocument();
		try {
			event.data= doc.get(fSelectionPosition.offset, fSelectionPosition.length);
		} catch (BadLocationException e) {
			event.detail= DND.DROP_NONE;
			event.doit= false;
		}
	}

	/*
	 * @see org.eclipse.swt.dnd.DragSourceListener#dragStart(org.eclipse.swt.dnd.DragSourceEvent)
	 */
	@Override
	public void dragStart(DragSourceEvent event) {
		if (!fIsEnabled) {
			event.doit= false;
			return;
		}
		// convert screen coordinates to widget offest
		int offset= getOffsetAtLocation(event.x, event.y, false);
		// convert further to a document offset
		offset= getDocumentOffset(offset);
		Point selection= fViewer.getSelectedRange();
		if (selection != null && offset >= selection.x && offset < selection.x+selection.y) {
			fSelectionPosition= new Position(selection.x, selection.y);
			if (fViewer instanceof ITextViewerExtension) {
				((ITextViewerExtension)fViewer).getRewriteTarget().beginCompoundChange();
			}
			IDocument doc= fViewer.getDocument();
			try {
				// add the drag selection position
				// the position is used to delete the selection on a DROP_MOVE
				// and it can be used by the drop target to determine if it should
				// allow the drop (e.g. if drop location overlaps selection)
				doc.addPositionCategory(DRAG_SELECTION_CATEGORY);
				fPositionUpdater= new DefaultPositionUpdater(DRAG_SELECTION_CATEGORY);
				doc.addPositionUpdater(fPositionUpdater);
				doc.addPosition(DRAG_SELECTION_CATEGORY, fSelectionPosition);
			} catch (BadLocationException e) {
				// should not happen
			} catch (BadPositionCategoryException e) {
				// cannot happen
			}
			event.doit= true;
			// this has no effect?
			event.detail = DND.DROP_COPY;
			if (isDocumentEditable()) {
				event.detail |= DND.DROP_MOVE;
			}
		} else {
			event.doit= false;
			event.detail = DND.DROP_NONE;
		}
	}

	/**
	 * Convert mouse screen coordinates to a <code>StyledText</code> offset.
	 * 
	 * @param x
	 *            screen X-coordinate
	 * @param y
	 *            screen Y-coordinate
	 * @param absolute
	 *            if <code>true</code>, coordinates are expected to be
	 *            absolute screen coordinates
	 * @return text offset
	 * 
	 * @see StyledText#getOffsetAtLocation()
	 */
	private int getOffsetAtLocation(int x, int y, boolean absolute) {
		StyledText textWidget= fViewer.getTextWidget();
		StyledTextContent content= textWidget.getContent();
		Point location;
		if (absolute) {
			location= textWidget.toControl(x, y);
		} else {
			location= new Point(x ,y);
		}
		int line= (textWidget.getTopPixel() + location.y) / textWidget.getLineHeight();
		if (line >= content.getLineCount()) {
			return content.getCharCount();
		}
		int lineOffset= content.getOffsetAtLine(line);
		String lineText= content.getLine(line);
		Point endOfLine= textWidget.getLocationAtOffset(lineOffset + lineText.length());
		if (location.x >= endOfLine.x) {
			return lineOffset + lineText.length();
		}
		try {
			return textWidget.getOffsetAtLocation(location);
		} catch (IllegalArgumentException iae) {
			// we are expecting this
			return -1;
		}
	}

	/**
	 * Convert a widget offset to the corresponding document offset.
	 * @param widgetOffset
	 * @return document offset
	 */
	private int getDocumentOffset(int widgetOffset) {
		if (fViewer instanceof ITextViewerExtension5) {
			ITextViewerExtension5 extension= (ITextViewerExtension5)fViewer;
			return extension.widgetOffset2ModelOffset(widgetOffset);
		}
		IRegion visible= fViewer.getVisibleRegion();
		if (widgetOffset > visible.getLength()) {
			return -1;
		}
		return widgetOffset + visible.getOffset();
	}

	/**
	 * @return true if the document may be changed by the drag.
	 */
	private boolean isDocumentEditable() {
		if (fEditor != null) {
			return !fEditor.isEditorInputReadOnly();
		}
		return fViewer.isEditable();
	}

	/**
	 * Enable or disable this drag listener.
	 * @param enabled
	 */
	public void setEnabled(boolean enabled) {
		fIsEnabled= enabled;
	}

}
