/**
 * Copyright (c) 2001-2020 Mathew A. Nelson and Robocode contributors
 * 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
 * https://robocode.sourceforge.io/license/epl-v10.html
 */
package net.sf.robocode.ui.editor;


import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.*;

import net.sf.robocode.ui.editor.theme.EditorThemePropertiesManager;
import net.sf.robocode.ui.editor.theme.EditorThemePropertyChangeAdapter;
import net.sf.robocode.ui.editor.theme.IEditorThemeProperties;
import net.sf.robocode.util.StringUtil;


// FIXME: Column in status bar does not take tab size into account

// TODO: Make it configurable to extend the Java keywords.
// TODO: Highlight methods from Robocode API?
// TODO: Highlight numbers?
// TODO: Method names and method invocations in bold?
// TODO: Trim trailing white-spaces from all lines?

/**
 * Represents a styled Java document used for syntax high-lightning.
 * 
 * @author Flemming N. Larsen (original)
 */
@SuppressWarnings("serial")
public class JavaDocument extends StyledDocument {

	/** The text pane this document is used with necessary for setting the caret position when auto indenting */
	private final EditorPane textPane;

	/** Flag defining if the contained text is being loaded or replaced externally */
	private boolean isReplacingText;

	// Indentation //

	/** Flag defining if auto indentation is enabled */
	private boolean isAutoIndentEnabled = true;

	/** Flag defining if spaces must be used for indentation; otherwise tabulation characters are being used */
	private boolean useSpacesForIndentation = false;

	/** Tab size (column width) */
	private int tabSize = 4; // Default is every 4th column

	/** Java language quote delimiter characters represented in a string */
	private static final String QUOTE_DELIMITERS = "\"'";

	/** Java keywords */
	private static final Set<String> KEYWORDS = new HashSet<String>(
			Arrays.asList(
					new String[] {
		"abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue",
		"default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if",
		"implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private",
		"protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this",
		"throw", "throws", "transient", "try", "void", "volatile", "while" }));

	/** Predefined Java literals */
	private static final Set<String> PREDEFINED_LITERALS = new HashSet<String>(
			Arrays.asList(new String[] { "false", "true", "null" }));

	/** Normal text attribute set */
	private final SimpleAttributeSet normalAttrSet = new SimpleAttributeSet();

	/** Quoted text attribute set */
	private final SimpleAttributeSet quoteAttrSet = new SimpleAttributeSet();

	/** Keyword attribute set */
	private final SimpleAttributeSet keywordAttrSet = new SimpleAttributeSet();

	/** Predefined literal attribute set */
	private final SimpleAttributeSet literalAttrSet = new SimpleAttributeSet();

	/** Annotation attribute set */
	private final SimpleAttributeSet annotationAttrSet = new SimpleAttributeSet();

	/** Comment attribute set */
	private SimpleAttributeSet commentAttrSet = new SimpleAttributeSet();

	/** String buffer holding only space characters for fast replacement of tabulator characters */
	private String spaceBuffer;

	/** Old start offset for syntax highlighting */
	private int lastSyntaxHighlightStartOffset;

	/** Old end offset for syntax highlighting */
	private int lastSyntaxHighlightEndOffset;

	private int autoIndentationCaretPos = -1;

	private boolean updateSyntaxHighlightingEDTidle = true;

	/**
	 * Constructor that creates a Java document.
	 * 
	 * @param textPane
	 *            is the text pane that this Java documents must apply to.
	 */
	public JavaDocument(EditorPane textPane) {
		super();
		this.textPane = textPane;

		// Setup text colors and styles
		setTextColorsAndStyles(null);

		// Setup document listener in order to update caret position and update syntax highlighting
		addDocumentListener(new JavaDocumentListener());

		// Setup editor properties change listener
		EditorThemePropertiesManager.addListener(new EditorThemePropertyChangeAdapter() {
			@Override
			public void onNormalTextColorChanged(Color newColor) {
				setNormalTextColor(newColor);
				updateSyntaxHighlighting(true);
			}

			@Override
			public void onNormalTextStyleChanged(FontStyle newStyle) {
				setNormalTextStyle(newStyle);
				updateSyntaxHighlighting(true);
			}

			@Override
			public void onQuotedTextColorChanged(Color newColor) {
				setQuotedTextColor(newColor);
				updateSyntaxHighlighting(true);
			}

			@Override
			public void onQuotedTextStyleChanged(FontStyle newStyle) {
				setQuotedTextStyle(newStyle);
				updateSyntaxHighlighting(true);
			}

			@Override
			public void onKeywordTextColorChanged(Color newColor) {
				setKeywordTextColor(newColor);
				updateSyntaxHighlighting(true);
			}

			@Override
			public void onKeywordTextStyleChanged(FontStyle newStyle) {
				setKeywordTextStyle(newStyle);
				updateSyntaxHighlighting(true);
			}

			@Override
			public void onLiteralTextColorChanged(Color newColor) {
				setLiteralTextColor(newColor);
				updateSyntaxHighlighting(true);
			}

			@Override
			public void onLiteralTextStyleChanged(FontStyle newStyle) {
				setLiteralTextStyle(newStyle);
				updateSyntaxHighlighting(true);
			}

			@Override
			public void onAnnotationTextColorChanged(Color newColor) {
				setAnnotationTextColor(newColor);
				updateSyntaxHighlighting(true);
			}

			@Override
			public void onAnnotationTextStyleChanged(FontStyle newStyle) {
				setAnnotationTextStyle(newStyle);
				updateSyntaxHighlighting(true);
			}

			@Override
			public void onCommentTextColorChanged(Color newColor) {
				setCommentTextColor(newColor);
				updateSyntaxHighlighting(true);
			}

			@Override
			public void onCommentTextStyleChanged(FontStyle newStyle) {
				setCommentTextStyle(newStyle);
				updateSyntaxHighlighting(true);
			}
		});
		
		// Setup change listener and focus listener on the viewport of the text pane

		JViewport viewport = textPane.getViewport();

		viewport.addChangeListener(new ChangeListener() {
			public void stateChanged(ChangeEvent e) {
				updateSyntaxHighlighting(false);
			}
		});

		viewport.addFocusListener(new FocusListener() {
			public void focusGained(FocusEvent e) {
				updateSyntaxHighlighting(false);
			}

			public void focusLost(FocusEvent e) {}
		});
	}

	/**
	 * Checks if auto indentation is enabled.
	 *
	 * @return true if auto indentation is enabled; false otherwise.
	 */
	public boolean isAutoIndentEnabled() {
		return isReplacingText ? false : isAutoIndentEnabled;
	}

	/**
	 * Enable or disable auto indentation.
	 *
	 * @param enable is set to true, if auto indentation must be enabled; false is auto indentation must be disabled.
	 */
	public void setAutoIndentEnabled(boolean enable) {
		isAutoIndentEnabled = enable;
	}

	/**
	 * Checks if the contained text is currently being replaced externally.
	 *
	 * @return true if the text is being replaced; false otherwise.
	 */
	public boolean isReplacingText() {
		return isReplacingText;
	}

	/**
	 * Sets the flag if the contained text is currently being replaced externally or not.
	 *
	 * @param isReplacingText is true if the text is currently being replaced; false otherwise.
	 */
	public void setReplacingText(boolean isReplacingText) {
		this.isReplacingText = isReplacingText;
	}
	
	@Override
	public void insertString(int offset, String str, AttributeSet a) throws BadLocationException {
		// Process auto indentation on inserted string
		final Indentation indent = applyAutoIndentation(offset, str);

		// Replace indentation tabulation characters with spaces if spaces must be used instead of tabulation characters
		str = replaceTabulatorCharacters(getEndOffset(getElementFromOffset(offset)), indent.text);

		// Set the new caret position on the text pane if the caret position has been set
		autoIndentationCaretPos = indent.caretPos;				

		// Insert the modified string using the original method for inserting string into this document
		super.insertString(offset, str, a);
	}

	/**
	 * Sets the tabulation column size.
	 * 
	 * @param tabSize
	 *            is the new tabulation column size, which must be >= 1.
	 */
	public void setTabSize(int tabSize) {
		this.tabSize = tabSize;
	}

	private void setTextColorsAndStyles(IEditorThemeProperties themeProps) {
		if (themeProps == null) {
			themeProps = EditorThemePropertiesManager.getCurrentEditorThemeProperties();			
		}
		setNormalTextColor(themeProps.getNormalTextColor());
		setNormalTextStyle(themeProps.getNormalTextStyle());
		setQuotedTextColor(themeProps.getQuotedTextColor());
		setQuotedTextStyle(themeProps.getQuotedTextStyle());
		setKeywordTextColor(themeProps.getKeywordTextColor());
		setKeywordTextStyle(themeProps.getKeywordTextStyle());
		setLiteralTextColor(themeProps.getLiteralTextColor());
		setLiteralTextStyle(themeProps.getLiteralTextStyle());
		setAnnotationTextColor(themeProps.getAnnotationTextColor());
		setAnnotationTextStyle(themeProps.getAnnotationTextStyle());
		setCommentTextColor(themeProps.getCommentTextColor());
		setCommentTextStyle(themeProps.getCommentTextStyle());
	}

	private void setNormalTextColor(Color newColor) {
		StyleConstants.setForeground(normalAttrSet, newColor);
	}

	private void setNormalTextStyle(FontStyle newStyle) {
		StyleConstants.setBold(normalAttrSet, newStyle.isBold());
		StyleConstants.setItalic(normalAttrSet, newStyle.isItalic());
	}
	
	private void setQuotedTextColor(Color newColor) {
		StyleConstants.setForeground(quoteAttrSet, newColor);
	}

	private void setQuotedTextStyle(FontStyle newStyle) {
		StyleConstants.setBold(quoteAttrSet, newStyle.isBold());
		StyleConstants.setItalic(quoteAttrSet, newStyle.isItalic());
	}

	private void setKeywordTextColor(Color newColor) {
		StyleConstants.setForeground(keywordAttrSet, newColor);
	}

	private void setKeywordTextStyle(FontStyle newStyle) {
		StyleConstants.setBold(keywordAttrSet, newStyle.isBold());
		StyleConstants.setItalic(keywordAttrSet, newStyle.isItalic());
	}

	private void setLiteralTextColor(Color newColor) {
		StyleConstants.setForeground(literalAttrSet, newColor);
	}

	private void setLiteralTextStyle(FontStyle newStyle) {
		StyleConstants.setBold(literalAttrSet, newStyle.isBold());
		StyleConstants.setItalic(literalAttrSet, newStyle.isItalic());
	}

	private void setAnnotationTextColor(Color newColor) {
		StyleConstants.setForeground(annotationAttrSet, newColor);
	}

	private void setAnnotationTextStyle(FontStyle newStyle) {
		StyleConstants.setBold(annotationAttrSet, newStyle.isBold());
		StyleConstants.setItalic(annotationAttrSet, newStyle.isItalic());
	}

	private void setCommentTextColor(Color newColor) {
		StyleConstants.setForeground(commentAttrSet, newColor);
	}

	private void setCommentTextStyle(FontStyle newStyle) {
		StyleConstants.setBold(commentAttrSet, newStyle.isBold());
		StyleConstants.setItalic(commentAttrSet, newStyle.isItalic());
	}
	
	/**
	 * Applies indentation for an inserted string at a given offset if auto indentation is enabled.
	 * 
	 * @param offset
	 *            is the offset of the inserted string.
	 * @param str
	 *            is the inserted string.
	 * @return an Indentation container with indentation details.
	 * @throws BadLocationException
	 */
	private Indentation applyAutoIndentation(int offset, String str) throws BadLocationException {
		// Prepare indentation container
		Indentation indentation = new Indentation();

		indentation.caretPos = -1; // Meaning that caret position is not changed
		indentation.text = str;

		// Apply auto indentation if it is enabled, and the new line character has been entered
		if (isAutoIndentEnabled() && str.equals("\n")) {

			// Save the current indentation for later, as it might change from here on
			int currentIndentation = getIndentationLengthFromOffset(offset);

			// Read the line content from the offset
			String line = getLineFromOffset(offset);

			// Calculate the end offset of the line
			int lineEndOffset = getElementFromOffset(offset).getStartOffset() + line.length();

			// Continue if the line has content and the offset is after the line end offset
			if (line.length() > 0 && offset >= lineEndOffset) {

				// Check/compose and return a body start indentation a '{' is found
				if (composeBodyStartIndentation(offset, line, indentation)) {
					return indentation;
				}
				// Check/compose and return a multiline comment start indentation if a '/*' is found
				if (composeMultilineCommentStartIndentation(offset, line, indentation)) {
					return indentation;
				}
				// Check/compose and return a continued multiline comment if a '*' is found inside a multiline comment
				if (composeMultilineCommentContinuedIndentation(offset, line, indentation)) {
					return indentation;
				}
				// Check/compose and return a multiline comment end indentation if a '*/' is found
				if (composeMultilineCommentEndIndentation(offset, line, indentation)) {
					return indentation;
				}
				// Extend the current indentation text based on the current indentation
				// Note: Will replace tabulator characters with spaces etc. if this must be done.
				indentation.text += getStartIndentation(currentIndentation); // FIXME
			}
		}
		return indentation;
	}

	/**
	 * Returns a string where tabulator characters in the given string is replaced with spaces, but only if these must
	 * be replaced with spaces by configuration.
	 * 
	 * @param startIndex
	 *            is the start index of the string in a line used for determine the current tabulator column.
	 * @param str
	 *            is the string containing tabulator characters that might need to be replaced with spaces.
	 * @return a string where tabulator characters might have been replaced with spaces.
	 */
	private String replaceTabulatorCharacters(int startIndex, String str) {
		// Check if tabulator characters needs to be replaced with space characters
		if (useSpacesForIndentation) {
			// Prepare string buffer containing replaced text
			StringBuilder sb = new StringBuilder();

			int tabIndex; // Index of current tabulator character

			// Run loop as long as we find a tabulator character from the start index
			while ((tabIndex = str.indexOf('\t', startIndex)) >= 0) {
				// Put the current text (non tabulator characters) into the string buffer
				sb.append(str.substring(startIndex, tabIndex));

				// Calculate the number of spaces that remain before the next tabulator column
				int numSpaces = tabSize - (sb.length() % tabSize);

				// Run loop while the follower character is a tabulator character
				while (++tabIndex < str.length() && str.charAt(tabIndex) == '\t') {
					// Increment the number of spaces that to use to replace tabulator characters with the tabulator
					// column size.
					numSpaces += tabSize;
				}
				// Append the calculated number of space characters to replace the tabulator characters
				sb.append(getSpaces(numSpaces));

				// The new start index is the current tabulator index
				startIndex = tabIndex;
			}
			// Append the text that remain to the string buffer from the current start index
			sb.append(str.substring(startIndex));

			// Set the result to the build string from the string buffer
			str = sb.toString();
		}
		// Return the resulting string
		return str;
	}

	/**
	 * Returns a string containing a specific number of spaces only.
	 * 
	 * @param count
	 *            is the number of spaces the the string should contain.
	 * @return a string containing only the given number of space characters.
	 */
	private String getSpaces(int count) {
		if (count == 0) {
			return "";
		}
		// Determine the current buffer size
		int bufferSize = (spaceBuffer == null) ? 0 : spaceBuffer.length();

		// Check if we need to reallocate the buffer to accommodate the given number of spaces
		if (count > bufferSize) {
			// Determine the new buffer size, which is set to twice the number of spaces, but minimum 100 characters
			bufferSize = Math.max(2 * count, 100);
			// Create a new string containing spaces with the new buffer size
			char[] chars = new char[bufferSize];

			Arrays.fill(chars, ' ');
			spaceBuffer = new String(chars);
		}
		// Return a string containing the given number of spaces
		return spaceBuffer.substring(0, count);
	}

	/**
	 * Composes a body start indentation block starting with a '{' and ending with a '}' and update the caret position.
	 * 
	 * @param offset
	 *            is the offset of the inserted string.
	 * @param line
	 *            is the line, where the indentation block must be appended to.
	 * @param indentation
	 *            is containing the current indentation date.
	 * @return true if a body start indentation block should be inserted after the given line; false otherwise.
	 * @throws BadLocationException
	 */
	private boolean composeBodyStartIndentation(int offset, String line, Indentation indentation)
		throws BadLocationException {
		// Check if the given line ends with a body start character, i.e. '{'
		if (line.endsWith("{")) {
			// We only start a new body indentation if the number of body start characters in the first part of the
			// text up to specified offset lesser than the number of body end characters in the last part of the text
			String textFirstHalf = getText(0, offset);
			String textLastHalf = getText(offset, getLength() - offset);
			if (StringUtil.countChar(textLastHalf, '}') >= StringUtil.countChar(textFirstHalf, '{')) {
				return false;
			}
			// Calculated current start indentation length from the given offset
			int startIndentLen = getIndentationLengthFromOffset(offset);
			// Calculate the start indentation string
			String startIndent = getStartIndentation(startIndentLen);
			// Prapare buffer for containing the indentation block
			StringBuilder sb = new StringBuilder("\n");

			// Append new indented line to the buffer, that is indented based on the start indentation
			sb.append(getStartIndentation(startIndentLen + tabSize)).append('\n');
			// Update the caret position to be placed in the end of the new indented line
			indentation.caretPos = offset + sb.toString().length() - 1;
			// Append the body end character on a new line
			sb.append(startIndent).append('}');
			// Set the indentation block text to the string containing from the buffer
			indentation.text = sb.toString();
			// Indentation block was created
			return true;
		}
		// Indentation block was not created
		return false;
	}

	/**
	 * Composes a multiline comment start indentation block.
	 * 
	 * @param offset
	 *            is the offset of the inserted string.
	 * @param line
	 *            is the line, where the indentation block must be appended to.
	 * @param indentation
	 *            is containing the current indentation date.
	 * @return true if a multiline comment start indentation block should be inserted after the given line; false
	 *         otherwise.
	 * @throws BadLocationException
	 */
	private boolean composeMultilineCommentStartIndentation(int offset, String line, Indentation indentation)
		throws BadLocationException {

		// Check if the given line contains a multiline start string, i.e. '/*'
		if (line.trim().startsWith("/*")) {
			// Calculated current start indentation length from the given offset
			int startIndentLen = getIndentationLengthFromOffset(offset);
			// Calculate the start indentation string
			String startIndent = getStartIndentation(startIndentLen);
			// Prepare buffer for containing the indentation block
			StringBuilder sb = new StringBuilder("\n");

			// Append new indented multiline comment line to the buffer, that is indented based on the start indentation
			sb.append(startIndent).append(" * \n");
			// Update the caret position to be placed in the end of the new indented line
			indentation.caretPos = offset + sb.toString().length() - 1;
			// Append the multiline comment end character on a new line
			sb.append(startIndent).append(" */");
			// Set the indentation block text to the string containing from the buffer
			indentation.text = sb.toString();
			// Indentation block was created
			return true;
		}
		// Indentation block was not created
		return false;
	}

	/**
	 * Composes a continued multiline comment indentation block.
	 * 
	 * @param offset
	 *            is the offset of the inserted string.
	 * @param line
	 *            is the line, where the indentation block must be appended to.
	 * @param indentation
	 *            is containing the current indentation date.
	 * @return true if a continued multiline comment indentation block should be inserted after the given line; false
	 *         otherwise.
	 * @throws BadLocationException
	 */
	private boolean composeMultilineCommentContinuedIndentation(int offset, String line, Indentation indentation)
		throws BadLocationException {

		// Check if the given line contains a '*' and is located inside of a multiline comment
		if (line.trim().startsWith("*") && isInMultilineComment(offset)) {
			// Calculated current start indentation length from the given offset
			int startIndentLen = getIndentationLengthFromOffset(offset);
			// Calculate the start indentation string
			String startIndent = getStartIndentation(startIndentLen);
			// Prepare buffer for containing the indentation block
			StringBuilder sb = new StringBuilder("\n");

			// Append new indented multiline comment line to the buffer, that is indented based on the start indentation
			sb.append(startIndent).append("* ");
			// Update the caret position to be placed in the end of the new indented line
			indentation.caretPos = offset + sb.toString().length();
			// Set the indentation block text to the string containing from the buffer
			indentation.text = sb.toString();
			// Indentation block was created
			return true;
		}
		// Indentation block was not created
		return false;
	}

	/**
	 * Composes a multiline end start indentation block.
	 * 
	 * @param offset
	 *            is the offset of the inserted string.
	 * @param line
	 *            is the line, where the indentation block must be appended to.
	 * @param indentation
	 *            is containing the current indentation date.
	 * @return true if a multiline comment end indentation block should be inserted after the given line; false
	 *         otherwise.
	 * @throws BadLocationException
	 */
	private boolean composeMultilineCommentEndIndentation(int offset, String line, Indentation indentation)
		throws BadLocationException {

		// Check if the given line contains a multiline end string, i.e. '*/'
		if (line.trim().endsWith("*/")) {
			// Get the line index from the current offset
			int lineIndex = getElementIndex(offset);

			// Run loop as long as the line index is still positive (till the start of the document text)
			while (lineIndex >= 0) {
				// Check if the current line line is not in a multiline comment
				int lineOffset = getElement(lineIndex).getStartOffset();

				if (!isInMultilineComment(lineOffset)) {
					// Append indentation that matches the start offset of the current line
					indentation.text += getStartIndentation(getIndentationLengthFromOffset(lineOffset));
					break;
				}
				// Move to previous line
				lineIndex--;
			}
			// Indentation block was created
			return true;
		}
		// Indentation block was not created
		return false;
	}

	/**
	 * Returns line from the specified offset where trailing white spaces are removed. Note: The white spaces in the
	 * beginning of the line is not removed as these serve to determine start indentation for inserted lines following
	 * this line.
	 * 
	 * @param offset
	 *            is the offset to retrieve the line from.
	 * @return the line at the given offset where trailing white spaces have been trimmed.
	 * @throws BadLocationException
	 */
	private String getLineFromOffset(int offset) throws BadLocationException {
		// Get the start and end index of the line
		Element element = getElementFromOffset(offset);
		int start = element.getStartOffset();
		int end = getEndOffset(element);

		if (end == start) {
			return "";
		}

		// Get the line content
		String origLine = getText(start, end - start);
		// Trim trailing white spaces
		String trimmedLine = origLine.replaceAll("\\s*$", "");

		// Return the trimmed line, but only if it did not contain only white spaces as we need the white spaces for
		// indentation for lines inserted after this line.
		return trimmedLine.length() > 0 ? trimmedLine : origLine;
	}

	/**
	 * Calculates and returns the indentation length (number of space characters) of the line at the given offset.
	 * 
	 * @param offset
	 *            is the document offset of the line.
	 * @return the calculated indentation length of the line at the given offset.
	 * @throws BadLocationException
	 */
	private int getIndentationLengthFromOffset(int offset) throws BadLocationException {
		// Gets the start offset of the line at the given offset
		int startOffset = getElementFromOffset(offset).getStartOffset();

		// Run loop while the next character is a space of tabulator character.
		int length = 0;

		for (;;) {
			// Get the next character
			char ch = getText(startOffset++, 1).charAt(0);

			if (ch == ' ') {
				// Increment the length by 1 if the character is a space
				length++;
			} else if (ch == '\t') {
				// Increment the length by the number of spaces the remain in order to reach the next tabulator column,
				// if the character is a tabulator character.
				length = tabSize * (length / tabSize + 1);
			} else {
				// Otherwise, stop looping
				break;
			}
		}
		// Return the calculated indentation length
		return length;
	}

	/**
	 * Returns a start indentation string of the given length.
	 * 
	 * @param length
	 *            is the length of the indentation measured in number of spaces.
	 * @return a string to apply to the start of a line in order to indent the line.
	 */
	private String getStartIndentation(int length) {
		if (length == 0) {
			return "";
		}

		// Prepare string buffer for containing the indentation string
		StringBuilder sb = new StringBuilder();

		// Check if tabulator characters must be used for indentation
		if (!useSpacesForIndentation) {
			// Append as many tabulator characters to the indentation string that fits into the given length
			for (int i = length / tabSize; i > 0; i--) {
				sb.append('\t');
			}
			// Set the length to the spaces remaining to reach the input length
			length %= tabSize;
		}
		// Apply spaces to the string buffer (might be the spaces that remain to fit the given length)
		sb.append(getSpaces(length));
		// Return the indentation string contained in the string buffer
		return sb.toString();
	}

	/**
	 * Updates the syntax highlighting on the document using the EDT.
	 */
	private void updateSyntaxHighlighting(final boolean force) {
		// Only invoke the EDT, if this operation is not already initiated
		if (updateSyntaxHighlightingEDTidle) {
			updateSyntaxHighlightingEDTidle = false;

			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					try {
						// Apply syntax highlighting from the current offset
						performSyntaxHighlighting(force);
						updateSyntaxHighlightingEDTidle = true;
					} catch (BadLocationException e) {
						e.printStackTrace();
					}
				}
			});
		}
	}

	/**
	 * Perform syntax highlighting on the document. This implementation only performs syntax highlighting on the current
	 * visible text in the view port of the text pane, and detects if the text has been changed before performing the
	 * syntax highlighting.
	 * 
	 * @throws BadLocationException
	 */
	private void performSyntaxHighlighting(boolean force) throws BadLocationException {
		// Return if there is nothing to highlight
		if (getLength() == 0 && !force) {
			return;
		}

		// Get the start and end offset of the visible text
		
		JViewport viewport = textPane.getViewport();
		Point startPoint = viewport.getViewPosition();
		Dimension size = viewport.getExtentSize();
		Point endPoint = new Point(startPoint.x + size.width, startPoint.y + size.height);

		int startOffset = textPane.viewToModel(startPoint);
		int endOffset = textPane.viewToModel(endPoint);

		// Return if the current start and end offset is equal to the last ones
		if (!force && startOffset == lastSyntaxHighlightStartOffset && endOffset == lastSyntaxHighlightEndOffset) {
			return;
		}

		lastSyntaxHighlightStartOffset = startOffset;
		lastSyntaxHighlightEndOffset = endOffset;

		setCharacterAttributes(startOffset, endOffset - startOffset, normalAttrSet, true);

		// Get start and end line
		int startLine = getElementIndex(startOffset);
		int endLine = getElementIndex(endOffset);

		// Process each changed line one by one from the start line to the end line
		for (int line = startLine; line <= endLine; line++) {
			processChangedLine(line);
		}
	}

	/**
	 * Process changed line by applying syntax highlighting on the entire line at the given index.
	 * 
	 * @param lineIndex
	 *            is the index of the line.
	 * @throws BadLocationException
	 */
	private void processChangedLine(int lineIndex) throws BadLocationException {
		// Process the syntax tokens on the given line
		Element element = getElement(lineIndex);

		processLineTokens(element.getStartOffset(), getEndOffset(element));
	}

	/**
	 * Process the syntax tokens contained in a line.
	 * 
	 * @param startOffset
	 *            is the document start offset of the line to process.
	 * @param endOffset
	 *            is the document end offset of the line to process.
	 * @throws BadLocationException
	 */
	private void processLineTokens(int startOffset, int endOffset) throws BadLocationException {
		// Calculate the length of the line based on the given start and end offset
		int len = endOffset - startOffset;

		// Process tokens one by one
		String textFragment = getText(startOffset, len);
		int index = 0;

		while (index < len) {
			index += processToken(textFragment.substring(index), startOffset + index);
		}
	}

	/**
	 * Process the syntax token contained in a text fragment.
	 * 
	 * @param textFragment
	 *            is the text fragment.
	 * @param startOffset
	 *            is the start offset of the text fragment to process.
	 * @return the number of processed characters.
	 * @throws BadLocationException
	 */
	private int processToken(final String textFragment, final int startOffset) throws BadLocationException {
		// Process quote token if the first character in the text fragment is a quote delimiter
		if (isQuoteDelimiter(textFragment.charAt(0))) {
			return processQuoteToken(textFragment, startOffset);
		}
		int len;

		// Check if the token is a single line comment
		// Note: Single line comment has higher precedence than a multiline comment.
		if (textFragment.startsWith("//")) {
			len = textFragment.length();
			setCharacterAttributes(startOffset, len, commentAttrSet, true);
			return len;
		}
		// Check if the token is a multiline comment
		if (textFragment.startsWith("/*")) {
			return processStartMultilineCommentToken(textFragment, startOffset);
		}
		// Check if the token is in the middle of a multiline comment
		if (isInMultilineComment(startOffset)) {
			// Check if the token contains the end mark of the multiline comment
			int endCommentIndex = textFragment.indexOf("*/");

			if (endCommentIndex >= 0) {
				len = endCommentIndex + 2; // Limit length to the end mark
			} else {
				len = textFragment.length(); // Use the whole token
			}
			setCharacterAttributes(startOffset, len, commentAttrSet, true);
			return len;
		}

		// Skip delimiter characters
		len = 1;
		while (len < textFragment.length()) {
			char ch = textFragment.charAt(len);

			if (!Character.isLetter(ch)) {
				break;
			}
			len++;
		}

		// Limit token to current length, e.g. if it contained delimiter characters
		String token = textFragment.substring(0, len);

		// Check if the token is a keyword or an annotation
		if (startOffset > 0 && !Character.isLetter(getText(startOffset - 1, 1).charAt(0)) || startOffset == 0) {
			if (isKeyword(token)) {
				setCharacterAttributes(startOffset, len, keywordAttrSet, true);
			} else if (isPredefinedLiteral(token)) {
				setCharacterAttributes(startOffset, len, literalAttrSet, true);
			} else if (isAnnotation(token)) {
				setCharacterAttributes(startOffset, len, annotationAttrSet, true);
			} else {
				len = 1;
			}
		} else {
			len = 1;
		}
		// Return the number of processed characters
		return len;
	}

	/**
	 * Process a quote token.
	 * 
	 * @param textFragment
	 *            is the text fragment.
	 * @param startOffset
	 *            is the start offset of the text fragment to process.
	 * @return the number of processed characters.
	 * @throws BadLocationException
	 */
	private int processQuoteToken(final String textFragment, final int startOffset) {
		char quoteDelimiter = textFragment.charAt(0);

		// Find end quote if it exists. Ignore escaped quotes
		int indexQuote = 1;
		int quoteEndIndex = -1;

		for (;;) {
			indexQuote = textFragment.indexOf(quoteDelimiter, indexQuote);
			if (indexQuote <= 0 || indexQuote > textFragment.length()) {
				break;
			}
			if (textFragment.charAt(indexQuote - 1) != '\\') {
				quoteEndIndex = indexQuote;
				break;
			}
			indexQuote++;
		}

		// Set length of token, if end quote was found
		int len = quoteEndIndex >= 0 ? quoteEndIndex + 1 : textFragment.length();

		// Set quote attribute set
		setCharacterAttributes(startOffset, len, quoteAttrSet, true);

		return len;
	}

	/**
	 * Process a multiline comment token.
	 * 
	 * @param textFragment
	 *            is the text fragment.
	 * @param startOffset
	 *            is the start offset of the text fragment to process.
	 * @return the number of processed characters.
	 * @throws BadLocationException
	 */
	private int processStartMultilineCommentToken(final String textFragment, final int startOffset) {
		int endIndex = textFragment.indexOf("*/", 1);
		int len = endIndex >= 0 ? endIndex + 2 : textFragment.length();

		setCharacterAttributes(startOffset, len, commentAttrSet, true);

		return len;
	}

	/**
	 * Checks if the current text offset is within a multiline comment.
	 * 
	 * @param offset
	 *            is the text offset.
	 * @return true if the given offset is within a multiline comment; false otherwise.
	 * @throws BadLocationException
	 */
	private boolean isInMultilineComment(final int offset) throws BadLocationException {
		return isInMultilineComment(getElementIndex(offset), offset);
	}

	/**
	 * Checks if the current text offset is within a multiline comment.
	 * 
	 * @oaram lineIndex is the element index of a line.
	 * @param offset
	 *            is the text offset.
	 * @return true if the given line element index and offset is within a multiline comment; false otherwise.
	 * @throws BadLocationException
	 */
	private boolean isInMultilineComment(final int lineIndex, final int offset) throws BadLocationException {
		int startOffset = getElement(lineIndex).getStartOffset();
		String lineText = getLineFromOffset(startOffset);
		int len = Math.min((offset - startOffset), lineText.length());

		lineText = lineText.substring(0, len);

		int commentStart = lineText.lastIndexOf("/*");
		int commentEnd = lineText.lastIndexOf("*/");

		if (commentStart >= 0) {
			if (commentEnd >= 0) {
				if (commentEnd > commentStart) {
					return (len < commentEnd);
				}
			} else {
				return true;
			}
		}
		if (commentEnd >= 0) {
			return false;
		}

		int prevLine = lineIndex - 1;

		return (prevLine >= 0) ? isInMultilineComment(prevLine, offset) : false;
	}

	/**
	 * Checks if a character is a quote delimiter character.
	 * 
	 * @param ch
	 *            is the character.
	 * @return true if the given character is a quote delimiter character; false otherwise.
	 */
	private static boolean isQuoteDelimiter(char ch) {
		return QUOTE_DELIMITERS.indexOf(ch) >= 0;
	}

	/**
	 * Checks if a token is a keyword.
	 * 
	 * @param token
	 *            is the token.
	 * @return true if the given token is a keyword; false otherwise.
	 */
	private static boolean isKeyword(String token) {
		return KEYWORDS.contains(token);
	}

	/**
	 * Checks if a token is a predefined literal.
	 * 
	 * @param token
	 *            is the token.
	 * @return true if the given token is a predefined literal; false otherwise.
	 */
	private static boolean isPredefinedLiteral(String token) {
		return PREDEFINED_LITERALS.contains(token);
	}

	/**
	 * Checks if a token is an annotation.
	 * 
	 * @param token
	 *            is the token.
	 * @return true if the given token is an annotation; false otherwise.
	 */
	private static boolean isAnnotation(String token) {
		return token.matches("@\\w.*");
	}

	/**
	 * Class containing data for indentation.
	 */
	private class Indentation {
		// Is the new caret position after the indentation has been made. -1 means no change.
		int caretPos;
		// Is the indentation text
		String text;
	}


	/**
	 * This document listener is used for updating the caret position and update syntax highlighting when the contents
	 * of the document is changed.
	 */
	private class JavaDocumentListener implements DocumentListener {

		// Caret position updater
		final CaretPositionUpdater caretPositionUpdater = new CaretPositionUpdater();
		
		public void insertUpdate(final DocumentEvent e) {
			int newCaretPosition;
			
			// Check if the caret position has been changed by auto indentation
			if (autoIndentationCaretPos >= 0) {
				// Set the new caret position to the one set for auto indentation
				newCaretPosition = autoIndentationCaretPos;
				// Signal that the caret position for auto indentation has been set
				autoIndentationCaretPos = -1;
			} else {
				// Set the new caret position to the end of the inserted text
				newCaretPosition = e.getOffset() + e.getLength();				
			}
			// Update the caret position to the new position
			caretPositionUpdater.updateCaretPosition(newCaretPosition);

			// Apply syntax highlighting from the current offset
			updateSyntaxHighlighting(false);
		}

		public void removeUpdate(final DocumentEvent e) {
			// Set the caret position where the text was removed.
			caretPositionUpdater.updateCaretPosition(e.getOffset());

			// Apply syntax highlighting from the current offset
			updateSyntaxHighlighting(false);
		}
		
		public void changedUpdate(DocumentEvent e) {}

		private final class CaretPositionUpdater {

			/**
			 * Updates the caret position via the EDT.
			 *
			 * @param newCaretPosition is the new caret position.
			 */
			public void updateCaretPosition(int newCaretPosition) {
				// Set the caret position, and take care that it is not out of range
				textPane.setCaretPosition(Math.min(newCaretPosition, getLength()));
			}
		}
	}
}
