/*******************************************************************************
 * Copyright (c) 2000, 2006 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
 *     Chris.Dennis@invidi.com - http://bugs.eclipse.org/bugs/show_bug.cgi?id=29027
 *******************************************************************************/
package org.eclipse.ui.texteditor;

import java.util.ResourceBundle;

import org.eclipse.swt.custom.StyledText;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.ISourceViewer;

/**
 * This action implements smart return. Instead of breaking the line where we
 * are, we do the following:
 * <p>
 * <b>Smart Enter</b>
 * </p>
 * <ul>
 * <li>if the caret is on a line containing any non-whitespace, a line is
 * inserted below the current one and the caret moved to it,</li>
 * <li>if the caret is on a whitespace-only line, a line is inserted below the
 * current line, but the caret stays in its position.</li>
 * </ul>
 * <p>
 * <b>Smart Enter Inverse</b>
 * </p>
 * <ul>
 * <li>if the caret is on a line containing any non-whitespace, we insert a line
 * above the current one and move the caret to it (i.e. it stays at the same
 * offset in the widget),</li>
 * <li>if the caret is on a whitespace-only line, a line is inserted above the
 * current line, but the caret stays in its logical position (i.e., it gets
 * shifted one line down in the document, but keeps its position relative to the
 * content following the caret).</li>
 * </ul>
 *
 * @since 3.0
 */
public class InsertLineAction extends TextEditorAction {

	/**
	 * <code>true</code> if this action inserts a line above the current (Smart Enter Inverse),
	 * <code>false</code> otherwise
	 */
	protected boolean fAbove;

	/**
	 * Creates a new smart enter action.
	 * @param bundle the resource bundle
	 * @param prefix the prefix to use to get properties from <code>bundle</code>
	 * @param textEditor the editor that the action acts upon
	 * @param above whether new lines are inserted above or below the caret's line.
	 */
	public InsertLineAction(ResourceBundle bundle, String prefix, ITextEditor textEditor, boolean above) {
		super(bundle, prefix, textEditor);
		fAbove= above;
	}

	@Override
	public void update() {
		super.update();
		if (isEnabled())
			setEnabled(canModifyEditor());
	}

	@Override
	public void run() {
		/*
		 * Implementation note: instead of computing any indentations needed
		 * (which we can't at this generic level), we simply insert a new
		 * line delimiter either at the end of the current line (normal) or
		 * the end of the previous line (reverse). By operating directly on
		 * the text widget, any auto-indent strategies can pick up on the
		 * delimiter and perform any content-dependent modifications.
		 */

		ITextEditor ed= getTextEditor();
		if (!(ed instanceof AbstractTextEditor))
			return;

		if (!validateEditorInputState())
			return;

		AbstractTextEditor editor= (AbstractTextEditor) ed;
		ISourceViewer sv= editor.getSourceViewer();
		if (sv == null)
			return;

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

		StyledText st= sv.getTextWidget();
		if (st == null || st.isDisposed())
			return;

		try {
			// get current line
			int widgetOffset= st.getCaretOffset();
			int offset= AbstractTextEditor.widgetOffset2ModelOffset(sv, widgetOffset);
			int currentLineNumber= document.getLineOfOffset(offset);
			IRegion currentLine= document.getLineInformation(currentLineNumber);

			int insertionOffset= -1;
			if (fAbove) {
				if (currentLineNumber != 0) {
					IRegion previousLine= document.getLineInformation(currentLineNumber - 1);
					insertionOffset= previousLine.getOffset() + previousLine.getLength();
				}
			} else {
				insertionOffset= currentLine.getOffset() + currentLine.getLength();
			}

			boolean updateCaret= true;
			int widgetInsertionOffset= AbstractTextEditor.modelOffset2WidgetOffset(sv, insertionOffset);
			if (widgetInsertionOffset == -1 && fAbove) {
				// assume that the previous line was not accessible
				// (e.g. folded, or we are on line 0)
				// -> we insert the newline at the beginning of the current line, after any leading WS
				insertionOffset= currentLine.getOffset() + getIndentationLength(document, currentLine);
				widgetInsertionOffset= AbstractTextEditor.modelOffset2WidgetOffset(sv, insertionOffset);
				updateCaret= false;
			}
			if (widgetInsertionOffset == -1)
				return;

			// mark caret
			Position caret= new Position(insertionOffset, 0);
			document.addPosition(caret);
			st.setSelectionRange(widgetInsertionOffset, 0);

			// operate directly on the widget
			st.replaceTextRange(widgetInsertionOffset, 0, st.getLineDelimiter());

			// restore caret unless an auto-indenter has already moved the caret
			// then leave it alone
			document.removePosition(caret);
			if (updateCaret && st.getSelection().x == widgetInsertionOffset) {
				int widgetCaret= AbstractTextEditor.modelOffset2WidgetOffset(sv, caret.getOffset());
				if (widgetCaret != -1)
					st.setSelectionRange(widgetCaret, 0);
				st.showSelection();
			}

		} catch (BadLocationException e) {
			// ignore
		}
	}

	/**
	 * Computes the indentation length of a line.
	 *
	 * @param document the document
	 * @param line the line
	 * @return the number of whitespace characters at the beginning of
	 *         <code>line</code>
	 * @throws BadLocationException on document access error
	 */
	private int getIndentationLength(IDocument document, IRegion line) throws BadLocationException {
		int pos= line.getOffset();
		int max= pos + line.getLength();
		while (pos < max) {
			if (!Character.isWhitespace(document.getChar(pos)))
				break;
			pos++;
		}
		return pos - line.getOffset();
	}
}
