/*******************************************************************************
 * Copyright (c) 2000, 2011 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
 *     Tom Eicher (Avaloq Evolution AG) - block selection mode
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.javaeditor;

import java.util.ResourceBundle;

import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.widgets.Event;

import org.eclipse.core.runtime.Assert;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.IRewriteTarget;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.source.ILineRange;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.LineRange;

import org.eclipse.ui.IEditorInput;

import org.eclipse.ui.texteditor.IEditorStatusLine;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
import org.eclipse.ui.texteditor.TextEditorAction;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.javaeditor.IndentUtil.IndentResult;


/**
 * Action for moving selected lines in a Java editor.
 * @since 3.1
 */
public class JavaMoveLinesAction extends TextEditorAction {

	/**
	 * State shared by the Move / Copy lines action quadruple.
	 * @since 3.1
	 */
	private static final class SharedState {
		/** The compilation unit editor that all four actions operate on. */
		public CompilationUnitEditor fEditor;
		/**
		 * The indent token shared by all four actions.
		 */
		public IndentResult fResult= null;

		/** <code>true</code> if a compound move / copy is going on. */
		private boolean fEditInProgress= false;
		/** The exit strategy that will detect the ending of a compound edit */
		private final CompoundEditExitStrategy fExitStrategy;

		public SharedState(CompilationUnitEditor editor) {
			fEditor= editor;
			fExitStrategy= new CompoundEditExitStrategy(new String[] {ITextEditorActionDefinitionIds.MOVE_LINES_UP, ITextEditorActionDefinitionIds.MOVE_LINES_DOWN, ITextEditorActionDefinitionIds.COPY_LINES_UP, ITextEditorActionDefinitionIds.COPY_LINES_DOWN});
			fExitStrategy.addCompoundListener(SharedState.this::endCompoundEdit);
		}

		/**
		 * Ends the compound change.
		 */
		public void beginCompoundEdit() {
			if (fEditInProgress || fEditor == null)
				return;

			fEditInProgress= true;

			fExitStrategy.arm(fEditor.getViewer());

			IRewriteTarget target= fEditor.getAdapter(IRewriteTarget.class);
			if (target != null) {
				target.beginCompoundChange();
			}
		}
		/**
		 * Ends the compound change.
		 */
		public void endCompoundEdit() {
			if (!fEditInProgress || fEditor == null)
				return;

			fExitStrategy.disarm();

			IRewriteTarget target= fEditor.getAdapter(IRewriteTarget.class);
			if (target != null) {
				target.endCompoundChange();
			}

			fResult= null;
			fEditInProgress= false;
		}
	}

	/* keys */

	/** Key for status message upon illegal move. <p>Value {@value}</p> */

	/* state variables - define what this action does */

	/** <code>true</code> if lines are shifted upwards, <code>false</code> otherwise. */
	private final boolean fUpwards;
	/** <code>true</code> if lines are to be copied instead of moved. */
	private final boolean fCopy;
	/** The shared state of the move/copy action quadruple. */
	private final SharedState fSharedState;

	/**
	 * Creates the quadruple of move and copy actions. The returned array contains
	 * the actions in the following order:
	 * [0] move up
	 * [1] move down
	 * [2] copy up (duplicate)
	 * [3] copy down (duplicate & select)
	 * @param bundle the resource bundle
	 * @param editor the editor
	 * @return the quadruple of actions
	 */
	public static JavaMoveLinesAction[] createMoveCopyActionSet(ResourceBundle bundle, CompilationUnitEditor editor) {
		SharedState state= new SharedState(editor);
		JavaMoveLinesAction[] actions= new JavaMoveLinesAction[4];
		actions[0]= new JavaMoveLinesAction(bundle, "Editor.MoveLinesUp.", true, false, state); //$NON-NLS-1$
		actions[1]= new JavaMoveLinesAction(bundle, "Editor.MoveLinesDown.", false, false, state); //$NON-NLS-1$
		actions[2]= new JavaMoveLinesAction(bundle, "Editor.CopyLineUp.", true, true, state); //$NON-NLS-1$
		actions[3]= new JavaMoveLinesAction(bundle, "Editor.CopyLineDown.", false, true, state); //$NON-NLS-1$
		return actions;
	}

	/*
	 * @see org.eclipse.ui.texteditor.TextEditorAction#setEditor(org.eclipse.ui.texteditor.ITextEditor)
	 */
	@Override
	public void setEditor(ITextEditor editor) {
		Assert.isTrue(editor instanceof CompilationUnitEditor);
		super.setEditor(editor);
		if (fSharedState != null)
			fSharedState.fEditor= (CompilationUnitEditor) editor;
	}

	/**
	 * Creates and initializes the action for the given text editor.
	 * The action configures its visual representation from the given resource
	 * bundle.
	 *
	 * @param bundle the resource bundle
	 * @param prefix a prefix to be prepended to the various resource keys
	 *   (described in <code>ResourceAction</code> constructor), or  <code>null</code> if none
	 * @param upwards <code>true</code>if the selected lines should be moved upwards,
	 * <code>false</code> if downwards
	 * @param copy if <code>true</code>, the action will copy lines instead of moving them
	 * @param state the shared state
	 * @see TextEditorAction#TextEditorAction(ResourceBundle, String, ITextEditor)
	 */
	private JavaMoveLinesAction(ResourceBundle bundle, String prefix, boolean upwards, boolean copy, SharedState state) {
		super(bundle, prefix, state.fEditor);
		fUpwards= upwards;
		fCopy= copy;
		fSharedState= state;
		update();
	}

	/**
	 * Checks if <code>selection</code> is contained by the visible region of <code>viewer</code>.
	 * As a special case, a selection is considered contained even if it extends over the visible
	 * region, but the extension stays on a partially contained line and contains only white space.
	 *
	 * @param selection the selection to be checked
	 * @param viewer the viewer displaying a visible region of <code>selection</code>'s document.
	 * @return <code>true</code>, if <code>selection</code> is contained, <code>false</code> otherwise.
	 */
	private boolean containedByVisibleRegion(ITextSelection selection, ISourceViewer viewer) {
		int min= selection.getOffset();
		int max= min + selection.getLength();
		IDocument document= viewer.getDocument();

		IRegion visible;
		if (viewer instanceof ITextViewerExtension5)
			visible= ((ITextViewerExtension5) viewer).getModelCoverage();
		else
			visible= viewer.getVisibleRegion();

		int visOffset= visible.getOffset();
		try {
			if (visOffset > min) {
				if (document.getLineOfOffset(visOffset) != selection.getStartLine())
					return false;
				if (!isWhitespace(document.get(min, visOffset - min))) {
					showStatus();
					return false;
				}
			}
			int visEnd= visOffset + visible.getLength();
			if (visEnd < max) {
				if (document.getLineOfOffset(visEnd) != selection.getEndLine())
					return false;
				if (!isWhitespace(document.get(visEnd, max - visEnd))) {
					showStatus();
					return false;
				}
			}
			return true;
		} catch (BadLocationException e) {
		}
		return false;
	}

	/**
	 * Given a selection on a document, computes the lines fully or partially covered by
	 * <code>selection</code>. A line in the document is considered covered if
	 * <code>selection</code> comprises any characters on it, including the terminating delimiter.
	 * <p>Note that the last line in a selection is not considered covered if the selection only
	 * comprises the line delimiter at its beginning (that is considered part of the second last
	 * line).
	 * As a special case, if the selection is empty, a line is considered covered if the caret is
	 * at any position in the line, including between the delimiter and the start of the line. The
	 * line containing the delimiter is not considered covered in that case.
	 * </p>
	 *
	 * @param document the document <code>selection</code> refers to
	 * @param selection a selection on <code>document</code>
	 * @param viewer the <code>ISourceViewer</code> displaying <code>document</code>
	 * @return a selection describing the range of lines (partially) covered by
	 * <code>selection</code>, without any terminating line delimiters
	 * @throws BadLocationException if the selection is out of bounds (when the underlying document has changed during the call)
	 */
	private ITextSelection getMovingSelection(IDocument document, ITextSelection selection, ISourceViewer viewer) throws BadLocationException {
		int low= document.getLineOffset(selection.getStartLine());
		int endLine= selection.getEndLine();
		int high= document.getLineOffset(endLine) + document.getLineLength(endLine);

		// get everything up to last line without its delimiter
		String delim= document.getLineDelimiter(endLine);
		if (delim != null)
			high -= delim.length();

		return new TextSelection(document, low, high - low);
	}

	/**
	 * Computes the region of the skipped line given the text block to be moved. If
	 * <code>fUpwards</code> is <code>true</code>, the line above <code>selection</code>
	 * is selected, otherwise the line below.
	 *
	 * @param document the document <code>selection</code> refers to
	 * @param selection the selection on <code>document</code> that will be moved.
	 * @return the region comprising the line that <code>selection</code> will be moved over, without its terminating delimiter.
	 */
	private ITextSelection getSkippedLine(IDocument document, ITextSelection selection) {
		int skippedLineN= (fUpwards ? selection.getStartLine() - 1 : selection.getEndLine() + 1);
		if (skippedLineN > document.getNumberOfLines() || (!fCopy && (skippedLineN < 0 ||  skippedLineN == document.getNumberOfLines())))
			return null;
		try {
			if (fCopy && skippedLineN == -1)
				skippedLineN= 0;
			IRegion line= document.getLineInformation(skippedLineN);
			return new TextSelection(document, line.getOffset(), line.getLength());
		} catch (BadLocationException e) {
			// only happens on concurrent modifications
			return null;
		}
	}

	/**
	 * Checks for white space in a string.
	 *
	 * @param string the string to be checked or <code>null</code>
	 * @return <code>true</code> if <code>string</code> contains only white space or is
	 * <code>null</code>, <code>false</code> otherwise
	 */
	private boolean isWhitespace(String string) {
		return string == null ? true : string.trim().length() == 0;
	}

	/*
	 * @see org.eclipse.jface.action.IAction#run()
	 */
	@Override
	public void runWithEvent(Event event) {

		// get involved objects
		if (fSharedState.fEditor == null)
			return;

		if (!validateEditorInputState())
			return;

		ISourceViewer viewer= fSharedState.fEditor.getViewer();
		if (viewer == null)
			return;

		IDocument document= viewer.getDocument();
		if (document == null)
			return;

		StyledText widget= viewer.getTextWidget();
		if (widget == null)
			return;

		// get selection
		ITextSelection sel= (ITextSelection) viewer.getSelectionProvider().getSelection();
		if (sel.isEmpty())
			return;

		ITextSelection skippedLine= getSkippedLine(document, sel);
		if (skippedLine == null)
			return;

		try {

			ITextSelection movingArea= getMovingSelection(document, sel, viewer);

			// if either the skipped line or the moving lines are outside the widget's
			// visible area, bail out
			if (!containedByVisibleRegion(movingArea, viewer) || !containedByVisibleRegion(skippedLine, viewer))
				return;

			// get the content to be moved around: the moving (selected) area and the skipped line
			String moving= movingArea.getText();
			String skipped= skippedLine.getText();
			if (moving == null || skipped == null || document.getLength() == 0)
				return;

			String delim;
			String insertion;
			int offset;
			if (fUpwards) {
				delim= document.getLineDelimiter(skippedLine.getEndLine());
				if (fCopy) {
					delim= TextUtilities.getDefaultLineDelimiter(document);
					insertion= moving + delim;
					offset= movingArea.getOffset();
				} else {
					Assert.isNotNull(delim);
					insertion= moving + delim + skipped;
					offset= skippedLine.getOffset();
				}
			} else {
				delim= document.getLineDelimiter(movingArea.getEndLine());
				if (fCopy) {
					if (delim == null) {
						delim= TextUtilities.getDefaultLineDelimiter(document);
						insertion= delim + moving;
					} else
						insertion= moving + delim;
					offset= skippedLine.getOffset();
				} else {
					Assert.isNotNull(delim);
					insertion= skipped + delim + moving;
					offset= movingArea.getOffset();
				}
			}
			int lenght= fCopy ? 0 : insertion.length();

			// modify the document
			ILineRange selectionBefore= getLineRange(document, movingArea);

			if (fCopy)
				fSharedState.endCompoundEdit();
			fSharedState.beginCompoundEdit();

			document.replace(offset, lenght, insertion);

			ILineRange selectionAfter;
			if (fUpwards && fCopy)
				selectionAfter= selectionBefore;
			else if (fUpwards)
				selectionAfter= new LineRange(selectionBefore.getStartLine() - 1, selectionBefore.getNumberOfLines());
			else if (fCopy)
				selectionAfter= new LineRange(selectionBefore.getStartLine() + selectionBefore.getNumberOfLines(), selectionBefore.getNumberOfLines());
			else
				selectionAfter= new LineRange(selectionBefore.getStartLine() + 1, selectionBefore.getNumberOfLines());

			fSharedState.fResult= IndentUtil.indentLines(document, selectionAfter, getProject(), fSharedState.fResult);

			// move the selection along
			IRegion region= getRegion(document, selectionAfter);
			selectAndReveal(viewer, region.getOffset(), region.getLength());

		} catch (BadLocationException x) {
			// won't happen without concurrent modification - bail out
			return;
		} finally {
			if (fCopy)
				fSharedState.endCompoundEdit();
		}
	}

	private IJavaProject getProject() {
		IEditorInput editorInput= fSharedState.fEditor.getEditorInput();
		ICompilationUnit unit= JavaPlugin.getDefault().getWorkingCopyManager().getWorkingCopy(editorInput);
		if (unit != null)
			return unit.getJavaProject();
		return null;
	}

	private ILineRange getLineRange(IDocument document, ITextSelection selection) throws BadLocationException {
		final int offset= selection.getOffset();
		int startLine= document.getLineOfOffset(offset);
		int endOffset= offset + selection.getLength();
		int endLine= document.getLineOfOffset(endOffset);
		final int nLines= endLine - startLine + 1;
		return new LineRange(startLine, nLines);
	}

	private IRegion getRegion(IDocument document, ILineRange lineRange) throws BadLocationException {
		final int startLine= lineRange.getStartLine();
		int offset= document.getLineOffset(startLine);
		final int numberOfLines= lineRange.getNumberOfLines();
		if (numberOfLines < 1)
			return new Region(offset, 0);
		int endLine= startLine + numberOfLines - 1;
		int endOffset;
		if (fSharedState.fEditor.isBlockSelectionModeEnabled()) {
			// in block selection mode, don't select the last delimiter as we count an empty selected line
			IRegion endLineInfo= document.getLineInformation(endLine);
			endOffset= endLineInfo.getOffset() + endLineInfo.getLength();
		} else {
			endOffset= document.getLineOffset(endLine) + document.getLineLength(endLine);
		}
		return new Region(offset, endOffset - offset);
	}

	/**
	 * Performs similar to AbstractTextEditor.selectAndReveal, but does not update
	 * the viewers highlight area.
	 *
	 * @param viewer the viewer that we want to select on
	 * @param offset the offset of the selection
	 * @param length the length of the selection
	 */
	private void selectAndReveal(ITextViewer viewer, int offset, int length) {
		// invert selection to avoid jumping to the end of the selection in st.showSelection()
		viewer.setSelectedRange(offset + length, -length);
		//viewer.revealRange(offset, length); // will trigger jumping
		StyledText st= viewer.getTextWidget();
		if (st != null)
			st.showSelection(); // only minimal scrolling
	}

	/**
	 * Displays information in the status line why a line move is not possible
	 */
	private void showStatus() {
		IEditorStatusLine status= fSharedState.fEditor.getAdapter(IEditorStatusLine.class);
		if (status == null)
			return;
		status.setMessage(false, JavaEditorMessages.Editor_MoveLines_IllegalMove_status, null);
	}

	/*
	 * @see org.eclipse.ui.texteditor.IUpdate#update()
	 */
	@Override
	public void update() {
		super.update();

		if (isEnabled())
			setEnabled(canModifyEditor());

	}
}
