/*
 * TextAreaPainter.java - Paints the text area
 * :tabSize=4:indentSize=4:noTabs=false:encoding=UTF-8:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 1999-2013 Slava Pestov, Shlomy Reinstein
 * Matthieu Casanova, Kazutoshi Satoda, Alan Ezust, Dale Anson
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package org.gjt.sp.jedit.textarea;

//{{{ Imports
import javax.swing.text.*;
import javax.swing.JComponent;

import java.awt.event.MouseEvent;
import java.awt.font.*;
import java.awt.*;
import java.util.*;
import org.gjt.sp.jedit.buffer.IndentFoldHandler;
import org.gjt.sp.jedit.buffer.JEditBuffer;
import org.gjt.sp.jedit.syntax.Chunk;
import org.gjt.sp.jedit.syntax.DefaultTokenHandler;
import org.gjt.sp.jedit.syntax.SyntaxStyle;
import org.gjt.sp.jedit.syntax.Token;
import org.gjt.sp.jedit.Debug;

import org.gjt.sp.util.Log;
//}}}

/**
 * The text area painter is the component responsible for displaying the
 * text of the current buffer. The only methods in this class that should
 * be called by plugins are those for adding and removing
 * text area extensions.
 *
 * @see #addExtension(TextAreaExtension)
 * @see #addExtension(int,TextAreaExtension)
 * @see #removeExtension(TextAreaExtension)
 * @see TextAreaExtension
 * @see TextArea
 *
 * @author Slava Pestov
 * @version $Id: TextAreaPainter.java 25251 2020-04-15 22:07:47Z kpouer $
 */
public class TextAreaPainter extends JComponent implements TabExpander
{
	//{{{ Layers
	/**
	 * The lowest possible layer.
	 * @see #addExtension(int,TextAreaExtension)
	 * @since jEdit 4.0pre4
	 */
	public static final int LOWEST_LAYER = Integer.MIN_VALUE;

	/**
	 * Below selection layer. The JDiff plugin will use this.
	 * @see #addExtension(int,TextAreaExtension)
	 * @since jEdit 4.0pre4
	 */
	public static final int BACKGROUND_LAYER = -60;

	/**
	 * The line highlight and collapsed fold highlight layer.
	 * @see #addExtension(int,TextAreaExtension)
	 * @since jEdit 4.0pre7
	 */
	public static final int LINE_BACKGROUND_LAYER = -50;

	/**
	 * Below selection layer.
	 * @see #addExtension(int,TextAreaExtension)
	 * @since jEdit 4.0pre4
	 */
	public static final int BELOW_SELECTION_LAYER = -40;

	/**
	 * Selection layer. Most extensions will be above this layer, but some
	 * (eg, JDiff) will want to be below the selection.
	 * @see #addExtension(int,TextAreaExtension)
	 * @since jEdit 4.0pre4
	 */
	public static final int SELECTION_LAYER = -30;

	/**
	 * Wrap guide layer. Most extensions will be above this layer.
	 * @since jEdit 4.0pre4
	 */
	public static final int WRAP_GUIDE_LAYER = -20;

	/**
	 * Below most extensions layer.
	 * @see #addExtension(int,TextAreaExtension)
	 * @since jEdit 4.0pre4
	 */
	public static final int BELOW_MOST_EXTENSIONS_LAYER = -10;

	/**
	 * Default extension layer. This is above the wrap guide but below the
	 * structure highlight.
	 * @since jEdit 4.0pre4
	 */
	public static final int DEFAULT_LAYER = 0;

	/**
	 * Block caret layer. Most extensions will be below this layer.
	 * @since jEdit 4.2pre1
	 */
	public static final int BLOCK_CARET_LAYER = 50;

	/**
	 * Bracket highlight layer. Most extensions will be below this layer.
	 * @since jEdit 4.0pre4
	 */
	public static final int BRACKET_HIGHLIGHT_LAYER = 100;

	/**
	 * Text layer. Most extensions will be below this layer.
	 * @since jEdit 4.2pre1
	 */
	public static final int TEXT_LAYER = 200;

	/**
	 * Caret layer. Most extensions will be below this layer.
	 * @since jEdit 4.2pre1
	 */
	public static final int CARET_LAYER = 300;

	/**
	 * Highest possible layer.
	 * @since jEdit 4.0pre4
	 */
	public static final int HIGHEST_LAYER = Integer.MAX_VALUE;
	//}}}

	//{{{ setBounds() method
	/**
	 * It is a bad idea to override this, but we need to get the component
	 * event before the first repaint.
	 */
	@Override
	public void setBounds(int x, int y, int width, int height)
	{
		// for some reason, the height is always off by one when the bounds have
		// not changed at all, hence the +1 on the height check. Without the +1, 
		// everything gets reset every time, which leads to other problems.
		if(x == getX() && y == getY() && width == getWidth()
			&& (height == getHeight() || height == getHeight() + 1))
		{
			return;
		}
		super.setBounds(x,y,width,height);

		textArea.recalculateVisibleLines();
		if(!textArea.getBuffer().isLoading())
			textArea.recalculateLastPhysicalLine();
		textArea.propertiesChanged();
		textArea.updateMaxHorizontalScrollWidth();
		textArea.scrollBarsInitialized = true;
	} //}}}

	//{{{ addNotify() method
	@Override
	public void addNotify()
	{
		super.addNotify();
		hiddenCursor = getToolkit().createCustomCursor(
			getGraphicsConfiguration()
			.createCompatibleImage(16,16,
			Transparency.BITMASK),
			new Point(0,0),"Hidden");
	} //}}}

	//{{{ setCursor() method
	/**
	 * Change the mouse cursor.
	 * If the cursor is hiddenCursor or TEXT_CURSOR, it is the default cursor and the cursor will not disappear
	 * anymore while typing until {@link #resetCursor()} is called.
	 * @param cursor the new cursor
	 * @since jEdit 4.4pre1
	 */
	@Override
	public void setCursor(Cursor cursor)
	{
		defaultCursor = cursor == hiddenCursor || cursor.getType() == Cursor.TEXT_CURSOR;
		super.setCursor(cursor);
	} //}}}

	//{{{ setCursor() method
	/**
	 * Reset the cursor to it's default value.
	 * @since jEdit 4.4pre1
	 */
	public void resetCursor()
	{
		defaultCursor = true;
	} //}}}

	//{{{ showCursor() method
	/**
	 * Show the cursor if it is the default cursor
	 */
	void showCursor()
	{
		if (defaultCursor)
		{
			   setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
		}
	} //}}}

	//{{{ hideCursor() method
	/**
	 * Hide the cursor if it is the default cursor
	 */
	void hideCursor()
	{
		if (defaultCursor)
		{
			setCursor(hiddenCursor);
		}
	} //}}}

	//{{{ getFocusTraversalKeysEnabled() method
	/**
	 * Makes the tab key work in Java 1.4.
	 * @since jEdit 3.2pre4
	 */
	@Override
	public boolean getFocusTraversalKeysEnabled()
	{
		return false;
	} //}}}

	//{{{ Getters and setters

	//{{{ getStyles() method
	/**
	 * Returns the syntax styles used to paint colorized text. Entry <i>n</i>
	 * will be used to paint tokens with id = <i>n</i>.
	 * @return an array of SyntaxStyles
	 * @see org.gjt.sp.jedit.syntax.Token
	 */
	public final SyntaxStyle[] getStyles()
	{
		return styles;
	} //}}}

	//{{{ setStyles() method
	/**
	 * Sets the syntax styles used to paint colorized text. Entry <i>n</i>
	 * will be used to paint tokens with id = <i>n</i>.
	 * @param styles The syntax styles
	 * @see org.gjt.sp.jedit.syntax.Token
	 */
	public final void setStyles(SyntaxStyle[] styles)
	{
		this.styles = styles;
		styles[Token.NULL] = new SyntaxStyle(getForeground(),null,getFont());
		textArea.chunkCache.reset();
		repaint();
	} //}}}

	//{{{ getCaretColor() method
	/**
	 * Returns the caret color.
	 */
	public final Color getCaretColor()
	{
		return caretColor;
	} //}}}

	//{{{ setCaretColor() method
	/**
	 * Sets the caret color.
	 * @param caretColor The caret color
	 */
	public final void setCaretColor(Color caretColor)
	{
		this.caretColor = caretColor;
		if(textArea.getBuffer() != null)
			textArea.invalidateLine(textArea.getCaretLine());
	} //}}}

	//{{{ getSelectionColor() method
	/**
	 * Returns the selection color.
	 */
	public final Color getSelectionColor()
	{
		return selectionColor;
	} //}}}

	//{{{ setSelectionColor() method
	/**
	 * Sets the selection color.
	 * @param selectionColor The selection color
	 */
	public final void setSelectionColor(Color selectionColor)
	{
		this.selectionColor = selectionColor;
		textArea.repaint();
	} //}}}

	//{{{ getMultipleSelectionColor() method
	/**
	 * Returns the multiple selection color.
	 * @since jEdit 4.2pre1
	 */
	public final Color getMultipleSelectionColor()
	{
		return multipleSelectionColor;
	} //}}}

	//{{{ setMultipleSelectionColor() method
	/**
	 * Sets the multiple selection color.
	 * @param multipleSelectionColor The multiple selection color
	 * @since jEdit 4.2pre1
	 */
	public final void setMultipleSelectionColor(Color multipleSelectionColor)
	{
		this.multipleSelectionColor = multipleSelectionColor;
		textArea.repaint();
	} //}}}

	//{{{ getLineHighlightColor() method
	/**
	 * Returns the line highlight color.
	 */
	public final Color getLineHighlightColor()
	{
		return lineHighlightColor;
	} //}}}

	//{{{ setLineHighlightColor() method
	/**
	 * Sets the line highlight color.
	 * @param lineHighlightColor The line highlight color
	 */
	public final void setLineHighlightColor(Color lineHighlightColor)
	{
		this.lineHighlightColor = lineHighlightColor;
		if(textArea.getBuffer() != null)
			textArea.invalidateLine(textArea.getCaretLine());
	} //}}}

	//{{{ isLineHighlightEnabled() method
	/**
	 * Returns true if line highlight is enabled, false otherwise.
	 */
	public final boolean isLineHighlightEnabled()
	{
		return lineHighlight;
	} //}}}

	//{{{ setLineHighlightEnabled() method
	/**
	 * Enables or disables current line highlighting.
	 * @param lineHighlight True if current line highlight should be enabled,
	 * false otherwise
	 */
	public final void setLineHighlightEnabled(boolean lineHighlight)
	{
		this.lineHighlight = lineHighlight;
		textArea.repaint();
	} //}}}

	//{{{ getSelectionFgColor() method
	/**
	 * Returns the selection foreground color, if one is set.
	 * @since jEdit 4.4.1
	 */
	public final Color getSelectionFgColor()
	{
		return selectionFgColor;
	} //}}}

	//{{{ setSelectionFgColor() method
	/**
	 * Sets the selection foreground color.
	 * @param selectionFgColor The selection foreground color
	 * @since jEdit 4.4.1
	 */
	public final void setSelectionFgColor(Color selectionFgColor)
	{
		this.selectionFgColor = selectionFgColor;
		if (isSelectionFgColorEnabled())
			textArea.repaint();
	} //}}}

	//{{{ isSelectionFgColorEnabled() method
	/**
	 * Returns true if selection foreground color is enabled - i.e. a specific
	 * color is used for the selection foreground instead of the syntax highlight
	 * color.
	 * @since jEdit 4.4.1
	 */
	public final boolean isSelectionFgColorEnabled()
	{
		return selectionFg;
	} //}}}

	//{{{ setSelectionFgColorEnabled() method
	/**
	 * Enables or disables selection foreground color.
	 * @param selectionFg True if selection foreground should be enabled,
	 * false otherwise
	 * @since jEdit 4.4.1
	 */
	public final void setSelectionFgColorEnabled(boolean selectionFg)
	{
		this.selectionFg = selectionFg;
		textArea.repaint();
	} //}}}

	//{{{ getStructureHighlightColor() method
	/**
	 * Returns the structure highlight color.
	 * @since jEdit 4.2pre3
	 */
	public final Color getStructureHighlightColor()
	{
		return structureHighlightColor;
	} //}}}

	//{{{ setStructureHighlightColor() method
	/**
	 * Sets the structure highlight color.
	 * @param structureHighlightColor The bracket highlight color
	 * @since jEdit 4.2pre3
	 */
	public final void setStructureHighlightColor(
		Color structureHighlightColor)
	{
		this.structureHighlightColor = structureHighlightColor;
		textArea.invalidateStructureMatch();
	} //}}}

	//{{{ isStructureHighlightEnabled() method
	/**
	 * Returns true if structure highlighting is enabled, false otherwise.
	 * @since jEdit 4.2pre3
	 */
	public final boolean isStructureHighlightEnabled()
	{
		return structureHighlight;
	} //}}}

	//{{{ setStructureHighlightEnabled() method
	/**
	 * Enables or disables structure highlighting.
	 * @param structureHighlight True if structure highlighting should be
	 * enabled, false otherwise
	 * @since jEdit 4.2pre3
	 */
	public final void setStructureHighlightEnabled(boolean structureHighlight)
	{
		this.structureHighlight = structureHighlight;
		textArea.invalidateStructureMatch();
	} //}}}

	//{{{ isBlockCaretEnabled() method
	/**
	 * Returns true if the caret should be drawn as a block, false otherwise.
	 */
	public final boolean isBlockCaretEnabled()
	{
		return blockCaret;
	} //}}}

	//{{{ setBlockCaretEnabled() method
	/**
	 * Sets if the caret should be drawn as a block, false otherwise.
	 * @param blockCaret True if the caret should be drawn as a block,
	 * false otherwise.
	 */
	public final void setBlockCaretEnabled(boolean blockCaret)
	{
		this.blockCaret = blockCaret;
		extensionMgr.removeExtension(caretExtension);
		if(blockCaret)
			addExtension(BLOCK_CARET_LAYER,caretExtension);
		else
			addExtension(CARET_LAYER,caretExtension);
		if(textArea.getBuffer() != null)
			textArea.invalidateLine(textArea.getCaretLine());
	} //}}}

	//{{{ isThickCaretEnabled() method
	/**
	 * Returns true if the caret should be drawn with a thick line, false otherwise.
	 * @since jEdit 4.3pre15
	 */
	public final boolean isThickCaretEnabled()
	{
		return thickCaret;
	} //}}}

	//{{{ setThickCaretEnabled() method
	/**
	 * Sets if the caret should be drawn with a thick line.
	 * @param thickCaret
	 *     True if the caret should be drawn as a block, false otherwise.
	 * @since jEdit 4.3pre15
	 */
	public final void setThickCaretEnabled(boolean thickCaret)
	{
		this.thickCaret = thickCaret;
		if(textArea.getBuffer() != null)
			textArea.invalidateLine(textArea.getCaretLine());
	} //}}}

	public String getEOLMarkerChar() {
		return eolMarkerChar;
	}
	public void setEOLMarkerChar(String emc) {
		eolMarkerChar = emc;
	}

	//{{{ getEOLMarkerColor() method
	/**
	 * Returns the EOL marker color.
	 */
	public final Color getEOLMarkerColor()
	{
		return eolMarkerColor;
	} //}}}

	//{{{ setEOLMarkerColor() method
	/**
	 * Sets the EOL marker color.
	 * @param eolMarkerColor The EOL marker color
	 */
	public final void setEOLMarkerColor(Color eolMarkerColor)
	{
		this.eolMarkerColor = eolMarkerColor;
		repaint();
	} //}}}

	//{{{ getEOLMarkersPainted() method
	/**
	 * Returns true if EOL markers are drawn, false otherwise.
	 */
	public final boolean getEOLMarkersPainted()
	{
		return eolMarkers;
	} //}}}

	//{{{ setEOLMarkersPainted() method
	/**
	 * Sets if EOL markers are to be drawn.
	 * @param eolMarkers True if EOL markers should be drawn, false otherwise
	 */
	public final void setEOLMarkersPainted(boolean eolMarkers)
	{
		this.eolMarkers = eolMarkers;
		repaint();
	} //}}}

	//{{{ getWrapGuideColor() method
	/**
	 * Returns the wrap guide color.
	 */
	public final Color getWrapGuideColor()
	{
		return wrapGuideColor;
	} //}}}

	//{{{ setWrapGuideColor() method
	/**
	 * Sets the wrap guide color.
	 * @param wrapGuideColor The wrap guide color
	 */
	public final void setWrapGuideColor(Color wrapGuideColor)
	{
		this.wrapGuideColor = wrapGuideColor;
		repaint();
	} //}}}

	//{{{ isWrapGuidePainted() method
	/**
	 * Returns true if the wrap guide is drawn, false otherwise.
	 * @since jEdit 4.0pre4
	 */
	public final boolean isWrapGuidePainted()
	{
		return wrapGuide;
	} //}}}

	//{{{ setWrapGuidePainted() method
	/**
	 * Sets if the wrap guide is to be drawn.
	 * @param wrapGuide True if the wrap guide should be drawn, false otherwise
	 */
	public final void setWrapGuidePainted(boolean wrapGuide)
	{
		this.wrapGuide = wrapGuide;
		repaint();
	} //}}}

	//{{{ getFoldLineStyle() method
	/**
	 * Returns the fold line style. The first element is the style for
	 * lines with a fold level greater than 3. The remaining elements
	 * are for fold levels 1 to 3.
	 */
	public final SyntaxStyle[] getFoldLineStyle()
	{
		return foldLineStyle;
	} //}}}

	//{{{ setFoldLineStyle() method
	/**
	 * Sets the fold line style. The first element is the style for
	 * lines with a fold level greater than 3. The remaining elements
	 * are for fold levels 1 to 3.
	 * @param foldLineStyle The fold line style
	 */
	public final void setFoldLineStyle(SyntaxStyle[] foldLineStyle)
	{
		this.foldLineStyle = foldLineStyle;
		textArea.chunkCache.reset();
		repaint();
	} //}}}

	//{{{ setAntiAliasEnabled() method
	/**
	 * As of jEdit 4.3, subpixel antialias mode is supported.
	 *
	 * @since jEdit 4.2pre4
	 */
	public void setAntiAlias(AntiAlias newValue)
	{
		antiAlias = newValue;
		updateRenderingHints();
	} //}}}

	//{{{ getAntiAlias() method
	/**
	 * @return the AntiAlias value that is currently used for TextAreas.
	 * @since jedit 4.3pre4
	 */
	public AntiAlias getAntiAlias()
	{
		return antiAlias;
	} //}}}

	//{{{ setFractionalFontMetricsEnabled() method
	/**
	 * Sets if fractional font metrics should be enabled. Has no effect when
	 * running on Java 1.1.
	 * @since jEdit 3.2pre6
	 */
	public void setFractionalFontMetricsEnabled(boolean fracFontMetrics)
	{
		this.fracFontMetrics = fracFontMetrics;
		updateRenderingHints();
	} //}}}

	//{{{ isFractionalFontMetricsEnabled() method
	/**
	 * Returns if fractional font metrics are enabled.
	 * @since jEdit 3.2pre6
	 */
	public boolean isFractionalFontMetricsEnabled()
	{
		return fracFontMetrics;
	} //}}}

	//{{{ getFontRenderContext() method
	/**
	 * Returns the font render context.
	 * @since jEdit 4.0pre4
	 */
	public FontRenderContext getFontRenderContext()
	{
		return fontRenderContext;
	} //}}}

	//}}}

	//{{{ addExtension() method
	/**
	 * Adds a text area extension, which can perform custom painting and
	 * tool tip handling.
	 * @param extension The extension
	 * @since jEdit 4.0pre4
	 */
	public void addExtension(TextAreaExtension extension)
	{
		extensionMgr.addExtension(DEFAULT_LAYER,extension);
		repaint();
	} //}}}

	//{{{ addExtension() method
	/**
	 * Adds a text area extension, which can perform custom painting and
	 * tool tip handling.
	 * @param layer The layer to add the extension to. Note that more than
	 * extension can share the same layer.
	 * @param extension The extension
	 * @since jEdit 4.0pre4
	 */
	public void addExtension(int layer, TextAreaExtension extension)
	{
		extensionMgr.addExtension(layer,extension);
		repaint();
	} //}}}

	//{{{ removeExtension() method
	/**
	 * Removes a text area extension. It will no longer be asked to
	 * perform custom painting and tool tip handling.
	 * @param extension The extension
	 * @since jEdit 4.0pre4
	 */
	public void removeExtension(TextAreaExtension extension)
	{
		extensionMgr.removeExtension(extension);
		repaint();
	} //}}}

	//{{{ getExtensions() method
	/**
	 * Returns an array of registered text area extensions. Useful for
	 * debugging purposes.
	 * @since jEdit 4.1pre5
	 */
	public TextAreaExtension[] getExtensions()
	{
		return extensionMgr.getExtensions();
	} //}}}

	//{{{ getToolTipText() method
	/**
	 * Returns the tool tip to display at the specified location.
	 * @param evt The mouse event
	 */
	@Override
	public String getToolTipText(MouseEvent evt)
	{
		if(textArea.getBuffer().isLoading())
			return null;

		return extensionMgr.getToolTipText(evt.getX(),evt.getY());
	} //}}}

	//{{{ getFontMetrics() method
	/**
	 * Returns the font metrics used by this component.
	 */
	public FontMetrics getFontMetrics()
	{
		return fm;
	} //}}}

	//{{{ getLineHeight() method
	/**
	 * Returns the line height as given by the font metrics plus the
	 * added line spacing.
	 */
	public int getLineHeight()
	{
		return fm.getHeight() + extraLineSpacing;
	} //}}}

	//{{{ getFontHeight() method
	/**
	 * Returns the font height as given by the font metrics.
	 */
	public int getFontHeight()
	{
		return fm.getHeight();
	} //}}}

	//{{{ getLineExtraSpacing() method
	/**
	 * Returns the number of pixels from the start of the line to the start
	 * of text (the extra line spacing).
	 */
	public int getLineExtraSpacing()
	{
		return extraLineSpacing;
	} //}}}

	//{{{ setLineExtraSpacing() method
	/**
	 * Sets extra spacing between lines in pixels.
	 */
	public void setLineExtraSpacing(int spacing)
	{
		extraLineSpacing = spacing;
	} //}}}

	//{{{ setFont() method
	/**
	 * Sets the font for this component. This is overridden to update the
	 * cached font metrics and to recalculate which lines are visible.
	 * @param font The font
	 */
	@Override
	public void setFont(Font font)
	{
		super.setFont(font);
		fm = getFontMetrics(font);
		textArea.recalculateVisibleLines();
		if(textArea.getBuffer() != null
			&& !textArea.getBuffer().isLoading())
			textArea.recalculateLastPhysicalLine();
	} //}}}

	//{{{ getStringWidth() method
	/**
	 * Returns the width of the given string, in pixels, using the text
	 * area's current font.
	 *
	 * @since jEdit 4.2final
	 */
	public float getStringWidth(String str)
	{
		if(textArea.charWidth != 0)
			return textArea.charWidth * str.length();
		else
		{
			return (float)getFont().getStringBounds(
				str,getFontRenderContext()).getWidth();
		}
	} //}}}

	//{{{ getRenderingHints() method
	/**
	 * Returns the rendering hints used by the Graphics2D class; in this
		 * case, for anti-aliasing of text.
	 *
	 * @since jEdit 4.5pre1
	 */
	public RenderingHints getRenderingHints()
	{
			return renderingHints;
		} //}}}

	//{{{ update() method
	/**
	 * Repaints the text.
	 * @param _gfx The graphics context
	 */
	@Override
	public void update(Graphics _gfx)
	{
		paint(_gfx);
	} //}}}

	//{{{ paint() method
	/**
	 * Repaints the text.
	 * @param _gfx The graphics context
	 */
	@Override
	public void paint(Graphics _gfx)
	{
		assert _gfx instanceof Graphics2D;
		Graphics2D gfx = (Graphics2D)_gfx;
		gfx.setRenderingHints(renderingHints);
		fontRenderContext = gfx.getFontRenderContext();

		Rectangle clipRect = gfx.getClipBounds();
		int lineHeight = getLineHeight();
		int charHeight = getFontHeight();
		if(lineHeight == 0 || textArea.getBuffer().isLoading())
		{
			gfx.setColor(getBackground());
			gfx.fillRect(clipRect.x,clipRect.y,clipRect.width,clipRect.height);
		}
		else
		{
			long prepareTime = System.nanoTime();
			// Because the clipRect's height is usually an even multiple
			// of the font height, we subtract 1 from it, otherwise one
			// too many lines will always be painted.
			int firstLine = clipRect.y / lineHeight;
			int lastLine = (clipRect.y + clipRect.height - 1) / lineHeight;
			gfx.setColor(getBackground());
			gfx.setFont(getFont());
			prepareTime = System.nanoTime() - prepareTime;

			long linesTime = System.nanoTime();
			int numLines = lastLine - firstLine + 1;
			int y = firstLine * lineHeight;
			gfx.fillRect(0,y,getWidth(),numLines * lineHeight);
			extensionMgr.paintScreenLineRange(textArea,gfx,
							  firstLine,lastLine,
							  y, lineHeight);
			linesTime = System.nanoTime() - linesTime;

			if(Debug.PAINT_TIMER && numLines >= 1)
				Log.log(Log.DEBUG,this,"repainting " + numLines + " lines took " + prepareTime + "/" + linesTime + " ns");
		}

		textArea.updateMaxHorizontalScrollWidth();
	} //}}}

	//{{{ nextTabStop() method
	/**
	 * Implementation of TabExpander interface. Returns next tab stop after
	 * a specified point.
	 * @param x The x co-ordinate
	 * @param tabOffset Ignored
	 * @return The next tab stop after <i>x</i>
	 */
	@Override
	public float nextTabStop(float x, int tabOffset)
	{
		int ntabs = (int)(x / textArea.tabSize);
		return (ntabs + 1) * textArea.tabSize;
	} //}}}

	//{{{ getPreferredSize() method
	/**
	 * Returns the painter's preferred size.
	 */
	@Override
	public Dimension getPreferredSize()
	{
		Dimension dim = new Dimension();

		char[] foo = new char[80];
		Arrays.fill(foo, ' ');
		dim.width = (int)getStringWidth(new String(foo));
		dim.height = getLineHeight() * 25;
		return dim;
	} //}}}

	//{{{ getMinimumSize() method
	/**
	 * Returns the painter's minimum size.
	 */
	@Override
	public Dimension getMinimumSize()
	{
		return getPreferredSize();
	} //}}}

	//{{{ Package-private members

	//{{{ Instance variables
	/* package-private since they are accessed by inner classes and we
	 * want this to be fast */
	TextArea textArea;

	SyntaxStyle[] styles;
	Color caretColor;
	Color selectionColor;
	Color multipleSelectionColor;
	Color lineHighlightColor;
	Color structureHighlightColor;
	Color eolMarkerColor;
	String eolMarkerChar;
	Color wrapGuideColor;

	SyntaxStyle[] foldLineStyle;

	boolean blockCaret;
	boolean thickCaret;
	boolean lineHighlight;
	boolean structureHighlight;
	boolean eolMarkers;
	boolean wrapGuide;
	AntiAlias antiAlias;
	boolean fracFontMetrics;
	RenderingHints renderingHints;
	boolean selectionFg;
	Color selectionFgColor;
	// should try to use this as little as possible.
	FontMetrics fm;
	int extraLineSpacing;
	//}}}

	//{{{ TextAreaPainter constructor
	/**
	 * Creates a new painter. Do not create instances of this class
	 * directly.
	 */
	TextAreaPainter(TextArea textArea)
	{
		enableEvents(AWTEvent.FOCUS_EVENT_MASK
			| AWTEvent.KEY_EVENT_MASK
			| AWTEvent.MOUSE_EVENT_MASK);

		this.textArea = textArea;
		antiAlias = new AntiAlias(0);
		extensionMgr = new ExtensionManager();
		eolMarkerChar = "·";
		setAutoscrolls(true);
		setOpaque(true);
		setRequestFocusEnabled(false);
		setDoubleBuffered(false);

		setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));

		fontRenderContext = new FontRenderContext(null,false,false);

		addExtension(LINE_BACKGROUND_LAYER,new PaintLineBackground());
		addExtension(SELECTION_LAYER,new PaintSelection());
		addExtension(WRAP_GUIDE_LAYER,new PaintWrapGuide());
		addExtension(BRACKET_HIGHLIGHT_LAYER,new StructureMatcher
			.Highlight(textArea));
		addExtension(TEXT_LAYER,new PaintText());
		addExtension(TEXT_LAYER,new PaintSelectionText());
		caretExtension = new PaintCaret();

		extraLineSpacing = 0;
	} //}}}

	//}}}

	//{{{ Private members

	//{{{ Instance variables
	private final ExtensionManager extensionMgr;
	private final PaintCaret caretExtension;
	private FontRenderContext fontRenderContext;
	private Cursor hiddenCursor;
	private boolean defaultCursor = true;
	//}}}

	//{{{ updateRenderingHints() method
	private void updateRenderingHints()
	{
		Map<RenderingHints.Key,Object> hints = new HashMap<RenderingHints.Key,Object>();

		hints.put(RenderingHints.KEY_FRACTIONALMETRICS,
			fracFontMetrics ? RenderingHints.VALUE_FRACTIONALMETRICS_ON
				: RenderingHints.VALUE_FRACTIONALMETRICS_OFF);

		hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, antiAlias.renderHint());


		if (antiAlias.val() == 0)
		{
			hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
			fontRenderContext = new FontRenderContext(null, antiAlias.val() > 0, fracFontMetrics);
		}
		/* Subpixel antialiasing mode */
		else if (antiAlias.val() > 1)
		{
			Object fontRenderHint = fracFontMetrics ?
				RenderingHints.VALUE_FRACTIONALMETRICS_ON :
				RenderingHints.VALUE_FRACTIONALMETRICS_OFF;
			fontRenderContext = new FontRenderContext(null, antiAlias.renderHint(),
				fontRenderHint);
		}
		else /* Standard Antialias Version */
		{
			hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
			hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
			hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
			fontRenderContext = new FontRenderContext(null, antiAlias.val() > 0, fracFontMetrics);
		}

		renderingHints = new RenderingHints(hints);
	} //}}}

	//}}}

	//{{{ Inner classes

	//{{{ PaintLineBackground class
	private class PaintLineBackground extends TextAreaExtension
	{
		//{{{ shouldPaintLineHighlight() method
		private boolean shouldPaintLineHighlight(int caret, int start, int end)
		{
			if(!isLineHighlightEnabled()
				|| caret < start || caret >= end)
			{
				return false;
			}

			int count = textArea.getSelectionCount();
			if(count == 1)
			{
				Selection s = textArea.getSelection(0);
				return s.getStartLine() == s.getEndLine();
			}
			else
				return count == 0;
		} //}}}

		//{{{ paintValidLine() method
		@Override
		public void paintValidLine(Graphics2D gfx, int screenLine,
			int physicalLine, int start, int end, int y)
		{
			// minimise access$ methods
			TextArea textArea = TextAreaPainter.this.textArea;
			JEditBuffer buffer = textArea.getBuffer();

			//{{{ Paint line highlight and collapsed fold highlight
			boolean collapsedFold =
				physicalLine < buffer.getLineCount() - 1
				&& buffer.isFoldStart(physicalLine)
				&& !textArea.displayManager
				.isLineVisible(physicalLine + 1);

			SyntaxStyle foldLineStyle = null;
			if(collapsedFold)
			{
				int level = buffer.getFoldLevel(physicalLine + 1);
				if(buffer.getFoldHandler() instanceof IndentFoldHandler)
					level = Math.max(1,level / buffer.getIndentSize());
				if(level > 3)
					level = 0;
				foldLineStyle = TextAreaPainter.this.foldLineStyle[level];
			}

			int caret = textArea.getCaretPosition();
			boolean paintLineHighlight = shouldPaintLineHighlight(
				caret,start,end);

			Color bgColor;
			if(paintLineHighlight)
				bgColor = lineHighlightColor;
			else if(collapsedFold)
			{
				bgColor = foldLineStyle.getBackgroundColor();
				if(bgColor == null)
					bgColor = getBackground();
			}
			else
				bgColor = getBackground();

			if(paintLineHighlight || collapsedFold)
			{
				gfx.setColor(bgColor);
				gfx.fillRect(0,y,getWidth(),getLineHeight());
			} //}}}

			//{{{ Paint token backgrounds
			ChunkCache.LineInfo lineInfo = textArea.chunkCache.getLineInfo(screenLine);

			if(lineInfo.chunks != null)
			{
				float baseLine = y + getLineHeight() - (fm.getLeading() + 1) - fm.getDescent();
				Chunk.paintChunkBackgrounds(
					lineInfo.chunks, 
					gfx,
					textArea.getHorizontalOffset(),
					baseLine, getLineHeight());
			} //}}}
		} //}}}
	} //}}}

	//{{{ PaintSelection class
	private class PaintSelection extends TextAreaExtension
	{
		//{{{ paintValidLine() method
		@Override
		public void paintValidLine(Graphics2D gfx, int screenLine,
			int physicalLine, int start, int end, int y)
		{
			if(textArea.getSelectionCount() == 0)
				return;

			gfx.setColor(textArea.isMultipleSelectionEnabled()
				? getMultipleSelectionColor()
				: getSelectionColor());

			Iterator<Selection> iter = textArea.getSelectionIterator();
			while(iter.hasNext())
			{
				Selection s = iter.next();
				paintSelection(gfx,screenLine,physicalLine,y,s);
			}
		} //}}}

		//{{{ paintSelection() method
		private void paintSelection(Graphics2D gfx, int screenLine,
			int physicalLine, int y, Selection s)
		{
			int[] selectionStartAndEnd
				= textArea.selectionManager
				.getSelectionStartAndEnd(
				screenLine,physicalLine,s);
			if(selectionStartAndEnd == null)
				return;

			int x1 = selectionStartAndEnd[0];
			int x2 = selectionStartAndEnd[1];

			gfx.fillRect(x1, y, x2 - x1, getLineHeight());
		} //}}}
	} //}}}

	//{{{ PaintSelectionText class
	private class PaintSelectionText extends TextAreaExtension
	{
		// All screen lines of the same physical line use the same indentation
		private float indent;
		private boolean indentFound = false;

		//{{{ paintValidLine() method
		@Override
		public void paintValidLine(Graphics2D gfx, int screenLine,
			int physicalLine, int start, int end, int y)
		{
			if(textArea.getSelectionCount() == 0)
				return;
			if ((! isSelectionFgColorEnabled()) || (getSelectionFgColor() == null))
				return;

			Iterator<Selection> iter = textArea.getSelectionIterator();
			while(iter.hasNext())
			{
				Selection s = iter.next();
				paintSelection(gfx,screenLine,physicalLine,y,s);
			}
		} //}}}

		//{{{ paintSelection() method
		private void paintSelection(Graphics2D gfx, int screenLine,
			int physicalLine, int y, Selection s)
		{
			if ((physicalLine < s.getStartLine()) || (physicalLine > s.getEndLine()))
				return;

			float x = indent = textArea.getHorizontalOffset();
			float baseLine = y + getLineHeight() -
				(fm.getLeading()+1) - fm.getDescent();

			DefaultTokenHandler tokenHandler = new DefaultTokenHandler();
			textArea.getBuffer().markTokens(physicalLine, tokenHandler);
			Token token = tokenHandler.getTokens();

			int lineStart = textArea.getLineStartOffset(physicalLine);
			int startOffset, endOffset;
			if (s instanceof Selection.Rect)
			{
				Selection.Rect r = (Selection.Rect)s;
				startOffset = r.getStart(textArea.getBuffer(), physicalLine);
				endOffset = r.getEnd(textArea.getBuffer(), physicalLine);
			}
			else
			{
				startOffset = Math.max(s.getStart(), lineStart);
				endOffset = s.getEnd();
			}
			// Soft-wrap
			int screenLineStart = textArea.getScreenLineStartOffset(screenLine);
			int screenLineEnd = textArea.getScreenLineEndOffset(screenLine);
			if (screenLineStart > startOffset)
				startOffset = screenLineStart;
			if (screenLineEnd < endOffset)
				endOffset = screenLineEnd;
			indentFound = false;

			int tokenStart = lineStart;
			while(token.id != Token.END)
			{
				int next = tokenStart + token.length;
				String sub = null;
				SyntaxStyle style = styles[token.id];
				if (next > startOffset)	// Reached selection start
				{
					if (tokenStart >= endOffset)	// Got past selection
						break;
					if(style != null)
					{
						gfx.setFont(style.getFont());
						gfx.setColor(getSelectionFgColor());
						int strStart;
						if (startOffset > tokenStart)
						{
							strStart = startOffset;
							x = nextX(x, style, sub, tokenStart, startOffset);
						}
						else
							strStart = tokenStart;
						int strEnd = Math.min(endOffset, next);
						sub = textArea.getText(strStart, strEnd - strStart);
						gfx.drawString(sub, x, baseLine);
						x = nextX(x, style, sub, strStart, strEnd);
					}
				}
				if (sub == null)
					x = nextX(x, style, null, tokenStart, next);
				tokenStart = next;
				token = token.next;
				if (tokenStart == screenLineStart)
					x = indent;
			}
		} //}}}

		//{{{
		float nextX(float x, SyntaxStyle style, String s, int startOffset,
			int endOffset)
		{
			if (s == null)
				s = textArea.getText(startOffset, endOffset - startOffset);
			if (s.equals("\t"))
			{
				int horzOffset = textArea.getHorizontalOffset();
				x = nextTabStop(x - horzOffset, endOffset) + horzOffset;
			}
			else
			{
				if ((! indentFound) && (! s.equals(" ")))
				{
					indentFound = true;
					indent = x;
				}
				Font font = (style != null) ? style.getFont() : getFont();
				x += font.getStringBounds(s, getFontRenderContext()).getWidth();
			}
			return x;
		}
	} //}}}

	//{{{ PaintWrapGuide class
	private class PaintWrapGuide extends TextAreaExtension
	{
		@Override
		public void paintScreenLineRange(Graphics2D gfx, int firstLine,
			int lastLine, int[] physicalLines, int[] start,
			int[] end, int y, int lineHeight)
		{
			if(textArea.wrapMargin != 0
				&& !textArea.wrapToWidth
				&& isWrapGuidePainted())
			{
				gfx.setColor(getWrapGuideColor());
				int x = textArea.getHorizontalOffset()
					+ textArea.wrapMargin;
				gfx.drawLine(x,y,x,y + (lastLine - firstLine
					+ 1) * lineHeight);
			}
		}

		@Override
		public String getToolTipText(int x, int y)
		{
			if(textArea.wrapMargin != 0
				&& !textArea.wrapToWidth
				&& isWrapGuidePainted())
			{
				int wrapGuidePos = textArea.wrapMargin
					+ textArea.getHorizontalOffset();
				if(Math.abs(x - wrapGuidePos) < 5)
				{
					return String.valueOf(textArea.getBuffer()
						.getProperty("maxLineLen"));
				}
			}

			return null;
		}
	} //}}}

	//{{{ PaintText class
	private class PaintText extends TextAreaExtension
	{
		@Override
		public void paintValidLine(Graphics2D gfx, int screenLine,
			int physicalLine, int start, int end, int y)
		{
			ChunkCache.LineInfo lineInfo = textArea.chunkCache.getLineInfo(screenLine);
			Font defaultFont = getFont();
			Color defaultColor = getForeground();

			gfx.setFont(defaultFont);
			gfx.setColor(defaultColor);

			int x = textArea.getHorizontalOffset();
			int originalX = x;

			float baseLine = y + getLineHeight() - (fm.getLeading()+1) - fm.getDescent();

			if(lineInfo.chunks != null)
			{
				x += Chunk.paintChunkList(
					lineInfo.chunks,
					gfx,
					textArea.getHorizontalOffset(),
					baseLine,
					!Debug.DISABLE_GLYPH_VECTOR);
			}

			JEditBuffer buffer = textArea.getBuffer();

			if(!lineInfo.lastSubregion)
			{
				gfx.setFont(defaultFont);
				gfx.setColor(eolMarkerColor);
				gfx.drawString(":",Math.max(x,
					textArea.getHorizontalOffset()
					+ textArea.wrapMargin + textArea.charWidth),
					baseLine);
				x += textArea.charWidth;
			}
			else if(physicalLine < buffer.getLineCount() - 1
				&& buffer.isFoldStart(physicalLine)
				&& !textArea.displayManager
				.isLineVisible(physicalLine + 1))
			{
				int level = buffer.getFoldLevel(physicalLine + 1);
				if(buffer.getFoldHandler() instanceof IndentFoldHandler)
					level = Math.max(1,level / buffer.getIndentSize());
				if(level > 3)
					level = 0;
				SyntaxStyle foldLineStyle = TextAreaPainter.this.foldLineStyle[level];

				Font font = foldLineStyle.getFont();
				gfx.setFont(font);
				gfx.setColor(foldLineStyle.getForegroundColor());

				int nextLine;
				int nextScreenLine = screenLine + 1;
				if(nextScreenLine < textArea.getVisibleLines())
				{
					nextLine = textArea.chunkCache.getLineInfo(nextScreenLine)
						.physicalLine;
				}
				else
				{
					nextLine = textArea.displayManager
						.getNextVisibleLine(physicalLine);
				}

				if(nextLine == -1)
					nextLine = textArea.getLineCount();

				int count = nextLine - physicalLine - 1;
				String str = " [" + count + " lines]";

				float width = getStringWidth(str);

				gfx.drawString(str,x,baseLine);
				x += width;
			}
			else if(eolMarkers)
			{
				gfx.setFont(defaultFont);
				gfx.setColor(eolMarkerColor);
				gfx.drawString(eolMarkerChar,x,baseLine);
				x += textArea.charWidth;
			}

			lineInfo.width = x - originalX;
		}
	} //}}}

	//{{{ PaintCaret class
	private class PaintCaret extends TextAreaExtension
	{
		@Override
		public void paintValidLine(Graphics2D gfx, int screenLine,
			int physicalLine, int start, int end, int y)
		{
			if(!textArea.isCaretVisible())
				return;

			int caret = textArea.getCaretPosition();
			if(caret < start || caret >= end)
				return;

			int offset = caret - textArea.getLineStartOffset(physicalLine);
			textArea.offsetToXY(physicalLine,
						offset, textArea.offsetXY);
			int caretX = textArea.offsetXY.x;
			int lineHeight = getLineHeight();
			int charHeight = Math.min(lineHeight, getFontHeight());
			int charOffset = Math.max(lineHeight - charHeight, 0);

			gfx.setColor(caretColor);

			if(textArea.isOverwriteEnabled())
			{
				if (thickCaret)
					gfx.fillRect(caretX, y + lineHeight - 4, textArea.charWidth, 3);

				else  gfx.drawLine(caretX, y + lineHeight - 1,
					caretX + textArea.charWidth, y + lineHeight - 1);
			}
			else if(blockCaret)
				gfx.drawRect(caretX, y + charOffset,
					textArea.charWidth - 1, charHeight - 1);
			else
			{
				if (thickCaret)
					gfx.fillRect(caretX, y + charOffset, 3, charHeight - 1);
				else
					gfx.drawLine(caretX, y + charOffset, caretX,
							 y + charOffset + charHeight - 1);
			}
		}
	} //}}}

	//}}}

	//}}}
}
