/*******************************************************************************
 * Copyright (c) 2000, 2004 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package net.sourceforge.phpdt.internal.ui.actions;

import java.util.Iterator;
import java.util.List;
import java.util.ResourceBundle;

import org.eclipse.jface.text.Assert;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPartitioningException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DefaultPositionUpdater;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension3;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRewriteTarget;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.texteditor.ITextEditorExtension2;
import org.eclipse.ui.texteditor.TextEditorAction;

/**
 * Common block comment code.
 * 
 * @since 3.0
 */
public abstract class BlockCommentAction extends TextEditorAction {

	/**
	 * Creates a new instance.
	 * 
	 * @param bundle
	 * @param prefix
	 * @param editor
	 */
	public BlockCommentAction(ResourceBundle bundle, String prefix,
			ITextEditor editor) {
		super(bundle, prefix, editor);
	}

	/**
	 * An edit is a kind of <code>DocumentEvent</code>, in this case an edit
	 * instruction, that is affilitated with a <code>Position</code> on a
	 * document. The offset of the document event is not stored statically, but
	 * taken from the affiliated <code>Position</code>, which gets updated
	 * when other edits occurr.
	 */
	static class Edit extends DocumentEvent {

		/**
		 * Factory for edits which manages the creation, installation and
		 * destruction of position categories, position updaters etc. on a
		 * certain document. Once a factory has been obtained, <code>Edit</code>
		 * objects can be obtained from it which will be linked to the document
		 * by positions of one position category.
		 * <p>
		 * Clients are required to call <code>release</code> once the
		 * <code>Edit</code>s are not used any more, so the positions can be
		 * discarded.
		 * </p>
		 */
		public static class EditFactory {

			/** The position category basename for this edits. */
			private static final String CATEGORY = "__positionalEditPositionCategory"; //$NON-NLS-1$

			/** The count of factories. */
			private static int fgCount = 0;

			/** This factory's category. */
			private final String fCategory;

			private IDocument fDocument;

			private IPositionUpdater fUpdater;

			/**
			 * Creates a new <code>EditFactory</code> with an unambiguous
			 * position category name.
			 * 
			 * @param document
			 *            the document that is being edited.
			 */
			public EditFactory(IDocument document) {
				fCategory = CATEGORY + fgCount++;
				fDocument = document;
			}

			/**
			 * Creates a new edition on the document of this factory.
			 * 
			 * @param offset
			 *            the offset of the edition at the point when is
			 *            created.
			 * @param length
			 *            the length of the edition (not updated via the
			 *            position update mechanism)
			 * @param text
			 *            the text to be replaced on the document
			 * @return an <code>Edit</code> reflecting the edition on the
			 *         document
			 */
			public Edit createEdit(int offset, int length, String text)
					throws BadLocationException {

				if (!fDocument.containsPositionCategory(fCategory)) {
					fDocument.addPositionCategory(fCategory);
					fUpdater = new DefaultPositionUpdater(fCategory);
					fDocument.addPositionUpdater(fUpdater);
				}

				Position position = new Position(offset);
				try {
					fDocument.addPosition(fCategory, position);
				} catch (BadPositionCategoryException e) {
					Assert.isTrue(false);
				}
				return new Edit(fDocument, length, text, position);
			}

			/**
			 * Releases the position category on the document and uninstalls the
			 * position updater. <code>Edit</code>s managed by this factory
			 * are not updated after this call.
			 */
			public void release() {
				if (fDocument != null
						&& fDocument.containsPositionCategory(fCategory)) {
					fDocument.removePositionUpdater(fUpdater);
					try {
						fDocument.removePositionCategory(fCategory);
					} catch (BadPositionCategoryException e) {
						Assert.isTrue(false);
					}
					fDocument = null;
					fUpdater = null;
				}
			}
		}

		/** The position in the document where this edit be executed. */
		private Position fPosition;

		/**
		 * Creates a new edition on <code>document</code>, taking its offset
		 * from <code>position</code>.
		 * 
		 * @param document
		 *            the document being edited
		 * @param length
		 *            the length of the edition
		 * @param text
		 *            the replacement text of the edition
		 * @param position
		 *            the position keeping the edition's offset
		 */
		protected Edit(IDocument document, int length, String text,
				Position position) {
			super(document, 0, length, text);
			fPosition = position;
		}

		/*
		 * @see org.eclipse.jface.text.DocumentEvent#getOffset()
		 */
		public int getOffset() {
			return fPosition.getOffset();
		}

		/**
		 * Executes the edition on document. The offset is taken from the
		 * position.
		 * 
		 * @throws BadLocationException
		 *             if the execution of the document fails.
		 */
		public void perform() throws BadLocationException {
			getDocument().replace(getOffset(), getLength(), getText());
		}

	}

	public void run() {
		if (!isEnabled())
			return;

		ITextEditor editor = getTextEditor();
		if (editor == null || !ensureEditable(editor))
			return;

		ITextSelection selection = getCurrentSelection();
		if (!isValidSelection(selection))
			return;

		if (!validateEditorInputState())
			return;

		IDocumentProvider docProvider = editor.getDocumentProvider();
		IEditorInput input = editor.getEditorInput();
		if (docProvider == null || input == null)
			return;

		IDocument document = docProvider.getDocument(input);
		if (document == null)
			return;

		IDocumentExtension3 docExtension;
		if (document instanceof IDocumentExtension3)
			docExtension = (IDocumentExtension3) document;
		else
			return;

		IRewriteTarget target = (IRewriteTarget) editor
				.getAdapter(IRewriteTarget.class);
		if (target != null) {
			target.beginCompoundChange();
		}

		Edit.EditFactory factory = new Edit.EditFactory(document);

		try {
			runInternal(selection, docExtension, factory);

		} catch (BadLocationException e) {
			// can happen on concurrent modification, deletion etc. of the
			// document
			// -> don't complain, just bail out
		} catch (BadPartitioningException e) {
			// should not happen
			Assert.isTrue(false, "bad partitioning"); //$NON-NLS-1$
		} finally {
			factory.release();

			if (target != null) {
				target.endCompoundChange();
			}
		}
	}

	/**
	 * Calls <code>perform</code> on all <code>Edit</code>s in
	 * <code>edits</code>.
	 * 
	 * @param edits
	 *            a list of <code>Edit</code>s
	 * @throws BadLocationException
	 *             if an <code>Edit</code> threw such an exception.
	 */
	protected void executeEdits(List edits) throws BadLocationException {
		for (Iterator it = edits.iterator(); it.hasNext();) {
			Edit edit = (Edit) it.next();
			edit.perform();
		}
	}

	/**
	 * Ensures that the editor is modifyable. If the editor is an instance of
	 * <code>ITextEditorExtension2</code>, its
	 * <code>validateEditorInputState</code> method is called, otherwise, the
	 * result of <code>isEditable</code> is returned.
	 * 
	 * @param editor
	 *            the editor to be checked
	 * @return <code>true</code> if the editor is editable, <code>false</code>
	 *         otherwise
	 */
	protected boolean ensureEditable(ITextEditor editor) {
		Assert.isNotNull(editor);

		if (editor instanceof ITextEditorExtension2) {
			ITextEditorExtension2 ext = (ITextEditorExtension2) editor;
			return ext.validateEditorInputState();
		}

		return editor.isEditable();
	}

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

		if (isEnabled()) {
			if (!canModifyEditor() || !isValidSelection(getCurrentSelection()))
				setEnabled(false);
		}
	}

	/**
	 * Returns the editor's selection, or <code>null</code> if no selection
	 * can be obtained or the editor is <code>null</code>.
	 * 
	 * @return the selection of the action's editor, or <code>null</code>
	 */
	protected ITextSelection getCurrentSelection() {
		ITextEditor editor = getTextEditor();
		if (editor != null) {
			ISelectionProvider provider = editor.getSelectionProvider();
			if (provider != null) {
				ISelection selection = provider.getSelection();
				if (selection instanceof ITextSelection)
					return (ITextSelection) selection;
			}
		}
		return null;
	}

	/**
	 * Runs the real command once all the editor, document, and selection checks
	 * have succeeded.
	 * 
	 * @param selection
	 *            the current selection we are being called for
	 * @param docExtension
	 *            the document extension where we get the partitioning from
	 * @param factory
	 *            the edit factory we can use to create <code>Edit</code>s
	 * @throws BadLocationException
	 *             if an edition fails
	 * @throws BadPartitioningException
	 *             if a partitioning call fails
	 */
	protected abstract void runInternal(ITextSelection selection,
			IDocumentExtension3 docExtension, Edit.EditFactory factory)
			throws BadLocationException, BadPartitioningException;

	/**
	 * Checks whether <code>selection</code> is valid.
	 * 
	 * @param selection
	 *            the selection to check
	 * @return <code>true</code> if the selection is valid, <code>false</code>
	 *         otherwise
	 */
	protected abstract boolean isValidSelection(ITextSelection selection);

	/**
	 * Returns the text to be inserted at the selection start.
	 * 
	 * @return the text to be inserted at the selection start
	 */
	protected String getCommentStart() {
		// for now: no space story
		return "/*"; //$NON-NLS-1$
	}

	/**
	 * Returns the text to be inserted at the selection end.
	 * 
	 * @return the text to be inserted at the selection end
	 */
	protected String getCommentEnd() {
		// for now: no space story
		return "*/"; //$NON-NLS-1$
	}

}
