/*******************************************************************************
 * Copyright (c) 2010, 2011 IBM Corporation and others.
 * All rights reserved. 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
 *******************************************************************************/
package org.eclipse.wst.sse.ui.internal.handlers;

import java.lang.reflect.InvocationTargetException;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentRewriteSession;
import org.eclipse.jface.text.DocumentRewriteSessionType;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension4;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.ui.StructuredTextEditor;
import org.eclipse.wst.sse.ui.internal.Logger;
import org.eclipse.wst.sse.ui.internal.SSEUIMessages;
import org.eclipse.wst.sse.ui.internal.StructuredTextViewer;
import org.eclipse.wst.sse.ui.internal.comment.BlockCommentingStrategy;
import org.eclipse.wst.sse.ui.internal.comment.CommentingStrategy;
import org.eclipse.wst.sse.ui.internal.comment.CommentingStrategyRegistry;
import org.eclipse.wst.sse.ui.internal.comment.LineCommentingStrategy;

/**
 * <p>A comment handler to toggle line comments, this means that if a
 * comment already exists on a line then toggling it will remove the comment,
 * if the line in question is not already commented then it will not be commented.
 * If multiple lines are selected each will be commented separately.  The handler
 * first attempts to find a {@link LineCommentingStrategy} for a line, if it can
 * not find one then it will try and find a {@link BlockCommentingStrategy} to
 * wrap just that line in.</p>
 * 
 * <p>If a great number of lines are being toggled then a progress dialog will be
 * displayed because this can be a timely process</p>
 */
public final class ToggleLineCommentHandler extends AbstractCommentHandler {
	/** if toggling more then this many lines then use a busy indicator */
	private static final int TOGGLE_LINES_MAX_NO_BUSY_INDICATOR = 10;
	
	/**
	 * @see org.eclipse.wst.sse.ui.internal.handlers.AbstractCommentHandler#processAction(
	 * 	org.eclipse.ui.texteditor.ITextEditor, org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument,
	 * 	org.eclipse.jface.text.ITextSelection)
	 */
	protected void processAction(ITextEditor textEditor,
			final IStructuredDocument document, ITextSelection textSelection) {
		
		IStructuredModel model = null;
		DocumentRewriteSession session = null;
		boolean changed = false;
		
		try {
			// get text selection lines info
			int selectionStartLine = textSelection.getStartLine();
			int selectionEndLine = textSelection.getEndLine();
			
			int selectionEndLineOffset = document.getLineOffset(selectionEndLine);
			int selectionEndOffset = textSelection.getOffset() + textSelection.getLength();

			// adjust selection end line
			if ((selectionEndLine > selectionStartLine) && (selectionEndLineOffset == selectionEndOffset)) {
				selectionEndLine--;
			}

			// save the selection position since it will be changing
			Position selectionPosition = null;
			selectionPosition = new Position(textSelection.getOffset(), textSelection.getLength());
			document.addPosition(selectionPosition);

			
			model = StructuredModelManager.getModelManager().getModelForEdit(document);
			if (model != null) {
				//makes it so one undo will undo all the edits to the document
				model.beginRecording(this, SSEUIMessages.ToggleComment_label, SSEUIMessages.ToggleComment_description);
				
				//keeps listeners from doing anything until updates are all done
				model.aboutToChangeModel();
				if(document instanceof IDocumentExtension4) {
					session = ((IDocumentExtension4)document).startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED);
				}
				changed = true;
				
				//get the display for the editor if we can
				Display display = null;
				if(textEditor instanceof StructuredTextEditor) {
					StructuredTextViewer viewer = ((StructuredTextEditor)textEditor).getTextViewer();
					if(viewer != null) {
						display = viewer.getControl().getDisplay();
					}
				}
				
				//create the toggling operation
				IRunnableWithProgress toggleCommentsRunnable = new ToggleLinesRunnable(
						model.getContentTypeIdentifier(), document, selectionStartLine, selectionEndLine, display);
				
				//if toggling lots of lines then use progress monitor else just run the operation
				if((selectionEndLine - selectionStartLine) > TOGGLE_LINES_MAX_NO_BUSY_INDICATOR && display != null) {
					ProgressMonitorDialog dialog = new ProgressMonitorDialog(display.getActiveShell());
					dialog.run(false, true, toggleCommentsRunnable);
				} else {
					toggleCommentsRunnable.run(new NullProgressMonitor());
				}
			}
		} catch (InvocationTargetException e) {
			Logger.logException("Problem running toggle comment progess dialog.", e);  //$NON-NLS-1$
		} catch (InterruptedException e) {
			Logger.logException("Problem running toggle comment progess dialog.", e);  //$NON-NLS-1$
		} catch (BadLocationException e) {
			Logger.logException("The given selection " + textSelection + " must be invalid", e); //$NON-NLS-1$ //$NON-NLS-2$
		} finally {
			//clean everything up
			if(session != null && document instanceof IDocumentExtension4) {
				((IDocumentExtension4)document).stopRewriteSession(session);
			}
			
			if(model != null) {
				model.endRecording(this);
				if(changed) {
					model.changedModel();
				}
				model.releaseFromEdit();
			}
		}
	}
	
	/**
	 * <p>The actual line toggling takes place in a runnable so it can be
	 * run as part of a progress dialog if there are many lines to toggle
	 * and thus the operation will take a noticeable amount of time the user
	 * should be aware of, this also allows for the operation to be canceled 
	 * by the user</p>
	 * 
	 */
	private static class ToggleLinesRunnable implements IRunnableWithProgress {
		/** the content type for the document being commented */
		private String fContentType;
		
		/** the document that the lines will be toggled on */
		private IStructuredDocument fDocument;
		
		/** the first line in the document to toggle */
		private int fSelectionStartLine;
		
		/** the last line in the document to toggle */
		private int fSelectionEndLine;
		
		/** the display, so that it can be updated during a long operation */
		private Display fDisplay;
		
		/**
		 * @param model {@link IStructuredModel} that the lines will be toggled on
		 * @param document {@link IDocument} that the lines will be toggled on
		 * @param selectionStartLine first line in the document to toggle
		 * @param selectionEndLine last line in the document to toggle
		 * @param display {@link Display}, so that it can be updated during a long operation
		 */
		protected ToggleLinesRunnable(String contentTypeIdentifier, IStructuredDocument document,
				int selectionStartLine, int selectionEndLine, Display display) {
			
			this.fContentType = contentTypeIdentifier;
			this.fDocument = document;
			this.fSelectionStartLine = selectionStartLine;
			this.fSelectionEndLine = selectionEndLine;
			this.fDisplay = display;
		}
		
		/**
		 * @see org.eclipse.jface.operation.IRunnableWithProgress#run(org.eclipse.core.runtime.IProgressMonitor)
		 */
		public void run(IProgressMonitor monitor) {
			//start work
			monitor.beginTask(SSEUIMessages.ToggleComment_progress,
					this.fSelectionEndLine-this.fSelectionStartLine);
			try {
				final CommentingStrategy[] strategies = new CommentingStrategy[fSelectionEndLine - fSelectionStartLine + 1];
				final int[] regions = new int[fSelectionEndLine - fSelectionStartLine + 1];
				boolean shouldComment = false;
				int strategyCount = 0;
				//toggle each line so long as task not canceled
				for (int line = this.fSelectionStartLine;
						line <= this.fSelectionEndLine && !monitor.isCanceled(); ++line) {
					
					//allows the user to be able to click the cancel button
					readAndDispatch(this.fDisplay);
					
					//get the line region
					IRegion lineRegion = this.fDocument.getLineInformation(line);
					
					//don't toggle empty lines
					String content = this.fDocument.get(lineRegion.getOffset(), lineRegion.getLength());
					if (content.trim().length() > 0) {
						//try to get a line comment type
						ITypedRegion[] lineTypedRegions =
							this.fDocument.computePartitioning(lineRegion.getOffset(), lineRegion.getLength());
						CommentingStrategy commentType = CommentingStrategyRegistry.getDefault().getLineCommentingStrategy(
								this.fContentType, lineTypedRegions);
						
						//could not find line comment type so find block comment type to use on line
						if(commentType == null) {
							commentType = CommentingStrategyRegistry.getDefault().getBlockCommentingStrategy(
									this.fContentType, lineTypedRegions);
						}
						
						//toggle the comment on the line
						if(commentType != null) {
							strategies[strategyCount] = commentType;
							regions[strategyCount++] = line;
							if (!shouldComment && !commentType.alreadyCommenting(this.fDocument, lineTypedRegions)) {
								shouldComment = true;
							}
						}
					}
					monitor.worked(1);
				}
				for (int i = 0; i < strategyCount; i++) {
					final IRegion lineRegion = fDocument.getLineInformation( regions[i] );
					if (shouldComment) {
						strategies[i].apply(this.fDocument, lineRegion.getOffset(), lineRegion.getLength());
					}
					else {
						strategies[i].remove(this.fDocument, lineRegion.getOffset(), lineRegion.getLength(), true);
					}
					monitor.worked(1);
				}
			} catch(BadLocationException e) {
				Logger.logException("Bad location while toggling comments.", e);  //$NON-NLS-1$
			}
			//done work
			monitor.done();
		}
		
		/**
		 * <p>When calling {@link Display#readAndDispatch()} the game is off as to whose code you maybe
		 * calling into because of event handling/listeners/etc.  The only important thing is that
		 * the UI has been given a chance to react to user clicks.  Thus the logging of most {@link Exception}s
		 * and {@link Error}s as caused by {@link Display#readAndDispatch()} because they are not caused
		 * by this code and do not effect it.</p>
		 * 
		 * @param display the {@link Display} to call <code>readAndDispatch</code>
		 * on with exception/error handling.
		 */
		private void readAndDispatch(Display display) {
			try {
				display.readAndDispatch();
			}
			catch (Exception e) {
				Logger.log(Logger.WARNING,
						"Exception caused by readAndDispatch, not caused by or fatal to caller", e);
			}
			catch (LinkageError e) {
				Logger.log(Logger.WARNING,
						"LinkageError caused by readAndDispatch, not caused by or fatal to caller", e);
			}
			catch (VirtualMachineError e) {
				// re-throw these
				throw e;
			}
			catch (ThreadDeath e) {
				// re-throw these
				throw e;
			}
			catch (Error e) {
				// catch every error, except for a few that we don't want to handle
				Logger.log(Logger.WARNING,
						"Error caused by readAndDispatch, not caused by or fatal to caller", e);
			}
		}
	}
}
