/*******************************************************************************
 * Copyright (c) 2000, 2007 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.jdt.internal.ui.text.comment;

import java.util.LinkedList;
import java.util.Map;

import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.TextEdit;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.TypedPosition;
import org.eclipse.jface.text.formatter.ContextBasedFormattingStrategy;
import org.eclipse.jface.text.formatter.FormattingContextProperties;
import org.eclipse.jface.text.formatter.IFormattingContext;

import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.compiler.IScanner;
import org.eclipse.jdt.core.compiler.ITerminalSymbols;
import org.eclipse.jdt.core.compiler.InvalidInputException;
import org.eclipse.jdt.core.formatter.CodeFormatter;
import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants;

import org.eclipse.jdt.ui.text.IJavaPartitions;

import org.eclipse.jdt.internal.ui.JavaPlugin;

/**
 * Formatting strategy for general source code comments.
 *
 * @since 3.0
 */
public class CommentFormattingStrategy extends ContextBasedFormattingStrategy {

	/** Documents to be formatted by this strategy */
	private final LinkedList fDocuments= new LinkedList();

	/** Partitions to be formatted by this strategy */
	private final LinkedList fPartitions= new LinkedList();

	/** Last formatted document's hash-code. */
	private int fLastDocumentHash;

	/** Last formatted document header's hash-code. */
	private int fLastHeaderHash;

	/** End of the first class or interface token in the last document. */
	private int fLastMainTokenEnd= -1;

	/** End of the header in the last document. */
	private int fLastDocumentsHeaderEnd;


	/*
	 * @see org.eclipse.jface.text.formatter.IFormattingStrategyExtension#format()
	 */
	public void format() {
		
		final IDocument document= (IDocument) fDocuments.getFirst();

		TextEdit edit= calculateTextEdit();
		if (edit == null)
			return;
		
		try {
			edit.apply(document);
		} catch (MalformedTreeException x) {
			JavaPlugin.log(x);
		} catch (BadLocationException x) {
			JavaPlugin.log(x);
		}
	}

	/**
	 * Calculates the <code>TextEdit</code> used to format the region with the 
	 * properties indicated in the formatting context previously supplied by 
	 * <code>formatterStarts(IFormattingContext)</code>.
	 * 
	 * @see CommentFormattingStrategy#format()
	 * @return A <code>TextEdit</code>, or <code>null</code> if no formating is required
	 * @since 3.3
	 */
	public TextEdit calculateTextEdit() {
		super.format();

		final IDocument document= (IDocument) fDocuments.removeFirst();
		final TypedPosition position= (TypedPosition)fPartitions.removeFirst();
		if (document == null || position == null)
			return null;

		Map preferences= getPreferences();
		final boolean isFormattingHeader= DefaultCodeFormatterConstants.TRUE.equals(preferences.get(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_HEADER));
		int documentsHeaderEnd= computeHeaderEnd(document, preferences);

		TextEdit edit= null;		
		if (position.offset >= documentsHeaderEnd) {
			// not a header
			try {
				// compute offset in document of region passed to the formatter
				int sourceOffset= document.getLineOffset(document.getLineOfOffset(position.getOffset()));

				// format region
				int partitionOffset= position.getOffset() - sourceOffset;
				int sourceLength= partitionOffset + position.getLength();
				String source= document.get(sourceOffset, sourceLength);
				CodeFormatter commentFormatter= ToolFactory.createCodeFormatter(preferences, ToolFactory.M_FORMAT_EXISTING);
				int indentationLevel= inferIndentationLevel(source.substring(0, partitionOffset), getTabSize(preferences), getIndentSize(preferences));
				edit= commentFormatter.format(getKindForPartitionType(position.getType()), source, partitionOffset, position.getLength(), indentationLevel, TextUtilities.getDefaultLineDelimiter(document));

				// move edit offset to match document
				if (edit != null)
					edit.moveTree(sourceOffset);
			} catch (BadLocationException x) {
				JavaPlugin.log(x);
			}
		} else if (isFormattingHeader) {
			boolean wasJavaDoc= DefaultCodeFormatterConstants.TRUE.equals(preferences.get(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_JAVADOC_COMMENT));
			if (!wasJavaDoc)
				preferences.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_JAVADOC_COMMENT, DefaultCodeFormatterConstants.TRUE);
			
			boolean wasBlockComment= DefaultCodeFormatterConstants.TRUE.equals(preferences.get(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_BLOCK_COMMENT));
			if (!wasBlockComment)
				preferences.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_BLOCK_COMMENT, DefaultCodeFormatterConstants.TRUE);
			
			boolean wasLineComment= DefaultCodeFormatterConstants.TRUE.equals(preferences.get(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_LINE_COMMENT));
			if (!wasLineComment)
				preferences.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_LINE_COMMENT, DefaultCodeFormatterConstants.TRUE);
			
			try {
				// compute offset in document of region passed to the formatter
				int sourceOffset= document.getLineOffset(document.getLineOfOffset(position.getOffset()));

				// format region
				int partitionOffset= position.getOffset() - sourceOffset;
				int sourceLength= partitionOffset + position.getLength();
				String source= document.get(sourceOffset, sourceLength);
				CodeFormatter commentFormatter= ToolFactory.createCodeFormatter(preferences);
				int indentationLevel= inferIndentationLevel(source.substring(0, partitionOffset), getTabSize(preferences), getIndentSize(preferences));
				edit= commentFormatter.format(getKindForPartitionType(position.getType()), source, partitionOffset, position.getLength(), indentationLevel, TextUtilities.getDefaultLineDelimiter(document));

				// move edit offset to match document
				if (edit != null)
					edit.moveTree(sourceOffset);
			} catch (BadLocationException x) {
				JavaPlugin.log(x);
			} finally {
				if (!wasJavaDoc)
					preferences.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_JAVADOC_COMMENT, DefaultCodeFormatterConstants.FALSE);
				if (!wasBlockComment)
					preferences.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_BLOCK_COMMENT, DefaultCodeFormatterConstants.FALSE);
				if (!wasLineComment)
					preferences.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_LINE_COMMENT, DefaultCodeFormatterConstants.FALSE);
			}

		}
		return edit;		
	}

	/*
	 * @see org.eclipse.jface.text.formatter.IFormattingStrategyExtension#formatterStarts(org.eclipse.jface.text.formatter.IFormattingContext)
	 */
	public void formatterStarts(IFormattingContext context) {
		super.formatterStarts(context);

		fPartitions.addLast(context.getProperty(FormattingContextProperties.CONTEXT_PARTITION));
		fDocuments.addLast(context.getProperty(FormattingContextProperties.CONTEXT_MEDIUM));
	}

	/*
	 * @see org.eclipse.jface.text.formatter.IFormattingStrategyExtension#formatterStops()
	 */
	public void formatterStops() {
		fPartitions.clear();
		fDocuments.clear();

		super.formatterStops();
	}

	/**
	 * Map from {@link IJavaPartitions}comment partition types to
	 * {@link CodeFormatter}code snippet kinds.
	 *
	 * @param type the partition type
	 * @return the code snippet kind
	 * @since 3.1
	 */
	private static int getKindForPartitionType(String type) {
		if (IJavaPartitions.JAVA_SINGLE_LINE_COMMENT.equals(type))
				return CodeFormatter.K_SINGLE_LINE_COMMENT;
		if (IJavaPartitions.JAVA_MULTI_LINE_COMMENT.equals(type))
				return CodeFormatter.K_MULTI_LINE_COMMENT;
		if (IJavaPartitions.JAVA_DOC.equals(type))
				return CodeFormatter.K_JAVA_DOC;
		return CodeFormatter.K_UNKNOWN;
	}

	/**
	 * Infer the indentation level based on the given reference indentation
	 * and tab size.
	 *
	 * @param reference the reference indentation
	 * @param tabSize the tab size
	 * @param indentSize the indent size in space equivalents
	 * @return the inferred indentation level
	 * @since 3.1
	 */
	private int inferIndentationLevel(String reference, int tabSize, int indentSize) {
		StringBuffer expanded= expandTabs(reference, tabSize);

		int referenceWidth= expanded.length();
		if (tabSize == 0)
			return referenceWidth;

		int level= referenceWidth / indentSize;
		if (referenceWidth % indentSize > 0)
			level++;
		return level;
	}

	/**
	 * Expands the given string's tabs according to the given tab size.
	 *
	 * @param string the string
	 * @param tabSize the tab size
	 * @return the expanded string
	 * @since 3.1
	 */
	private static StringBuffer expandTabs(String string, int tabSize) {
		StringBuffer expanded= new StringBuffer();
		for (int i= 0, n= string.length(), chars= 0; i < n; i++) {
			char ch= string.charAt(i);
			if (ch == '\t') {
				for (; chars < tabSize; chars++)
					expanded.append(' ');
				chars= 0;
			} else {
				expanded.append(ch);
				chars++;
				if (chars >= tabSize)
					chars= 0;
			}

		}
		return expanded;
	}

	/**
	 * Returns the visual tab size.
	 *
	 * @param preferences the preferences
	 * @return the visual tab size
	 * @since 3.1
	 */
	private static int getTabSize(Map preferences) {
		/*
		 * If the tab-char is SPACE, FORMATTER_INDENTATION_SIZE is not used
		 * by the core formatter.
		 * We piggy back the visual tab length setting in that preference in
		 * that case. See CodeFormatterUtil.
		 */
		String key;
		if (JavaCore.SPACE.equals(preferences.get(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR)))
			key= DefaultCodeFormatterConstants.FORMATTER_INDENTATION_SIZE;
		else
			key= DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE;

		if (preferences.containsKey(key))
			try {
				return Integer.parseInt((String) preferences.get(key));
			} catch (NumberFormatException e) {
				// use default
			}
		return 4;
	}
	
	/**
	 * Returns the indentation size in space equivalents.
	 *
	 * @param preferences the preferences
	 * @return the indentation size in space equivalents
	 * @since 3.3
	 */
	private static int getIndentSize(Map preferences) {
		/*
		 * FORMATTER_INDENTATION_SIZE is only used if FORMATTER_TAB_CHAR is MIXED. Otherwise, the
		 * indentation size is in FORMATTER_TAB_CHAR. See CodeFormatterUtil.
		 */
		String key;
		if (DefaultCodeFormatterConstants.MIXED.equals(preferences.get(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR)))
			key= DefaultCodeFormatterConstants.FORMATTER_INDENTATION_SIZE;
		else
			key= DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE;
		
		if (preferences.containsKey(key))
			try {
				return Integer.parseInt((String) preferences.get(key));
			} catch (NumberFormatException e) {
				// use default
			}
			return 4;
	}

	/**
	 * Returns the end offset for the document's header.
	 *
	 * @param document the document
	 * @param preferences the given preferences to format
	 * @return the header's end offset
	 */
	private int computeHeaderEnd(IDocument document, Map preferences) {
		if (document == null)
			return -1;

		try {
			if (fLastMainTokenEnd >= 0 && document.hashCode() == fLastDocumentHash && fLastMainTokenEnd < document.getLength() && document.get(0, fLastMainTokenEnd).hashCode() == fLastHeaderHash)
				return fLastDocumentsHeaderEnd;
		} catch (BadLocationException e) {
			// should not happen -> recompute
		}

		IScanner scanner= ToolFactory.createScanner(true, false, false, (String) preferences.get(JavaCore.COMPILER_SOURCE), (String) preferences.get(JavaCore.COMPILER_COMPLIANCE));
		scanner.setSource(document.get().toCharArray());

		try {
			int offset= -1;
			boolean foundComment= false;
			int terminal= scanner.getNextToken();
			while (terminal != ITerminalSymbols.TokenNameEOF && !(terminal == ITerminalSymbols.TokenNameclass || terminal == ITerminalSymbols.TokenNameinterface || terminal == ITerminalSymbols.TokenNameenum || (foundComment && (terminal == ITerminalSymbols.TokenNameimport || terminal == ITerminalSymbols.TokenNamepackage)))) {

				if (terminal == ITerminalSymbols.TokenNameCOMMENT_JAVADOC)
					offset= scanner.getCurrentTokenStartPosition();

				foundComment= terminal == ITerminalSymbols.TokenNameCOMMENT_JAVADOC || terminal == ITerminalSymbols.TokenNameCOMMENT_BLOCK;

				terminal= scanner.getNextToken();
			}

			int mainTokenEnd= scanner.getCurrentTokenEndPosition();
			if (terminal != ITerminalSymbols.TokenNameEOF) {
				mainTokenEnd++;
				if (offset == -1 || (foundComment && (terminal == ITerminalSymbols.TokenNameimport || terminal == ITerminalSymbols.TokenNamepackage)))
					offset= scanner.getCurrentTokenStartPosition();
			} else
				offset= -1;

			try {
				fLastHeaderHash= document.get(0, mainTokenEnd).hashCode();
			} catch (BadLocationException e) {
				// should not happen -> recompute next time
				mainTokenEnd= -1;
			}

			fLastDocumentHash= document.hashCode();
			fLastMainTokenEnd= mainTokenEnd;
			fLastDocumentsHeaderEnd= offset;
			return offset;

		} catch (InvalidInputException ex) {
			// enable formatting
			return -1;
		}
	}
}
