/*
 * TokenMarker.java - Generic token marker
 * Copyright (C) 1998, 1999 Slava Pestov
 *
 * You may use and modify this package for any purpose. Redistribution is
 * permitted, in both source and binary form, provided that this notice
 * remains intact in all source distributions of this package.
 */
package com.ochafik.swing.syntaxcoloring;
import javax.swing.text.Segment;

/**
 * A token marker that splits lines of text into tokens. Each token carries
 * a length field and an indentification tag that can be mapped to a color
 * for painting that token.<p>
 *
 * For performance reasons, the linked list of tokens is reused after each
 * line is tokenized. Therefore, the return value of <code>markTokens</code>
 * should only be used for immediate painting. Notably, it cannot be
 * cached.
 *
 * @author Slava Pestov
 * @version $Id: TokenMarker.java,v 1.32 1999/12/13 03:40:30 sp Exp $
 *
 * @see Token
 */
public abstract class TokenMarker
{
	/**
	 * A wrapper for the lower-level <code>markTokensImpl</code> method
	 * that is called to split a line up into tokens.
	 * @param line The line
	 * @param lineIndex The line number
	 */
	public Token markTokens(Segment line, int lineIndex)
	{
		if(lineIndex >= length)
		{
			throw new IllegalArgumentException("Tokenizing invalid line: "
				+ lineIndex);
		}

		lastToken = null;

		LineInfo info = lineInfo[lineIndex];
		LineInfo prev;
		if(lineIndex == 0)
			prev = null;
		else
			prev = lineInfo[lineIndex - 1];

		byte oldToken = info.token;
		byte token = markTokensImpl(prev == null ?
			Token.NULL : prev.token,line,lineIndex);

		info.token = token;

		/*
		 * This is a foul hack. It stops nextLineRequested
		 * from being cleared if the same line is marked twice.
		 *
		 * Why is this necessary? It's all JEditTextArea's fault.
		 * When something is inserted into the text, firing a
		 * document event, the insertUpdate() method shifts the
		 * caret (if necessary) by the amount inserted.
		 *
		 * All caret movement is handled by the select() method,
		 * which eventually pipes the new position to scrollTo()
		 * and calls repaint().
		 *
		 * Note that at this point in time, the new line hasn't
		 * yet been painted; the caret is moved first.
		 *
		 * scrollTo() calls offsetToX(), which tokenizes the line
		 * unless it is being called on the last line painted
		 * (in which case it uses the text area's painter cached
		 * token list). What scrollTo() does next is irrelevant.
		 *
		 * After scrollTo() has done it's job, repaint() is
		 * called, and eventually we end up in paintLine(), whose
		 * job is to paint the changed line. It, too, calls
		 * markTokens().
		 *
		 * The problem was that if the line started a multiline
		 * token, the first markTokens() (done in offsetToX())
		 * would set nextLineRequested (because the line end
		 * token had changed) but the second would clear it
		 * (because the line was the same that time) and therefore
		 * paintLine() would never know that it needed to repaint
		 * subsequent lines.
		 *
		 * This bug took me ages to track down, that's why I wrote
		 * all the relevant info down so that others wouldn't
		 * duplicate it.
		 */
		 if(!(lastLine == lineIndex && nextLineRequested))
			nextLineRequested = (oldToken != token);

		lastLine = lineIndex;

		addToken(0,Token.END);

		return firstToken;
	}

	/**
	 * An abstract method that splits a line up into tokens. It
	 * should parse the line, and call <code>addToken()</code> to
	 * add syntax tokens to the token list. Then, it should return
	 * the initial token type for the next line.<p>
	 *
	 * For example if the current line contains the start of a 
	 * multiline comment that doesn't end on that line, this method
	 * should return the comment token type so that it continues on
	 * the next line.
	 *
	 * @param token The initial token type for this line
	 * @param line The line to be tokenized
	 * @param lineIndex The index of the line in the document,
	 * starting at 0
	 * @return The initial token type for the next line
	 */
	protected abstract byte markTokensImpl(byte token, Segment line,
		int lineIndex);

	/**
	 * Returns if the token marker supports tokens that span multiple
	 * lines. If this is true, the object using this token marker is
	 * required to pass all lines in the document to the
	 * <code>markTokens()</code> method (in turn).<p>
	 *
	 * The default implementation returns true; it should be overridden
	 * to return false on simpler token markers for increased speed.
	 */
	public boolean supportsMultilineTokens()
	{
		return true;
	}

	/**
	 * Informs the token marker that lines have been inserted into
	 * the document. This inserts a gap in the <code>lineInfo</code>
	 * array.
	 * @param index The first line number
	 * @param lines The number of lines 
	 */
	public void insertLines(int index, int lines)
	{
		if(lines <= 0)
			return;
		length += lines;
		ensureCapacity(length);
		int len = index + lines;
		System.arraycopy(lineInfo,index,lineInfo,len,
			lineInfo.length - len);

		for(int i = index + lines - 1; i >= index; i--)
		{
			lineInfo[i] = new LineInfo();
		}
	}
	
	/**
	 * Informs the token marker that line have been deleted from
	 * the document. This removes the lines in question from the
	 * <code>lineInfo</code> array.
	 * @param index The first line number
	 * @param lines The number of lines
	 */
	public void deleteLines(int index, int lines)
	{
		if (lines <= 0)
			return;
		int len = index + lines;
		length -= lines;
		System.arraycopy(lineInfo,len,lineInfo,
			index,lineInfo.length - len);
	}

	/**
	 * Returns the number of lines in this token marker.
	 */
	public int getLineCount()
	{
		return length;
	}

	/**
	 * Returns true if the next line should be repainted. This
	 * will return true after a line has been tokenized that starts
	 * a multiline token that continues onto the next line.
	 */
	public boolean isNextLineRequested()
	{
		return nextLineRequested;
	}

	// protected members

	/**
	 * The first token in the list. This should be used as the return
	 * value from <code>markTokens()</code>.
	 */
	protected Token firstToken;

	/**
	 * The last token in the list. New tokens are added here.
	 * This should be set to null before a new line is to be tokenized.
	 */
	protected Token lastToken;

	/**
	 * An array for storing information about lines. It is enlarged and
	 * shrunk automatically by the <code>insertLines()</code> and
	 * <code>deleteLines()</code> methods.
	 */
	protected LineInfo[] lineInfo;

	/**
	 * The number of lines in the model being tokenized. This can be
	 * less than the length of the <code>lineInfo</code> array.
	 */
	protected int length;

	/**
	 * The last tokenized line.
	 */
	protected int lastLine;

	/**
	 * True if the next line should be painted.
	 */
	protected boolean nextLineRequested;

	/**
	 * Creates a new <code>TokenMarker</code>. This DOES NOT create
	 * a lineInfo array; an initial call to <code>insertLines()</code>
	 * does that.
	 */
	protected TokenMarker()
	{
		lastLine = -1;
	}

	/**
	 * Ensures that the <code>lineInfo</code> array can contain the
	 * specified index. This enlarges it if necessary. No action is
	 * taken if the array is large enough already.<p>
	 *
	 * It should be unnecessary to call this under normal
	 * circumstances; <code>insertLine()</code> should take care of
	 * enlarging the line info array automatically.
	 *
	 * @param index The array index
	 */
	protected void ensureCapacity(int index)
	{
		if(lineInfo == null)
			lineInfo = new LineInfo[index + 1];
		else if(lineInfo.length <= index)
		{
			LineInfo[] lineInfoN = new LineInfo[(index + 1) * 2];
			System.arraycopy(lineInfo,0,lineInfoN,0,
					 lineInfo.length);
			lineInfo = lineInfoN;
		}
	}

	/**
	 * Adds a token to the token list.
	 * @param length The length of the token
	 * @param id The id of the token
	 */
	protected void addToken(int length, byte id)
	{
		if(id >= Token.INTERNAL_FIRST && id <= Token.INTERNAL_LAST)
			throw new InternalError("Invalid id: " + id);

		if(length == 0 && id != Token.END)
			return;

		if(firstToken == null)
		{
			firstToken = new Token(length,id);
			lastToken = firstToken;
		}
		else if(lastToken == null)
		{
			lastToken = firstToken;
			firstToken.length = length;
			firstToken.id = id;
		}
		else if(lastToken.next == null)
		{
			lastToken.next = new Token(length,id);
			lastToken = lastToken.next;
		}
		else
		{
			lastToken = lastToken.next;
			lastToken.length = length;
			lastToken.id = id;
		}
	}

	/**
	 * Inner class for storing information about tokenized lines.
	 */
	public class LineInfo
	{
		/**
		 * Creates a new LineInfo object with token = Token.NULL
		 * and obj = null.
		 */
		public LineInfo()
		{
		}

		/**
		 * Creates a new LineInfo object with the specified
		 * parameters.
		 */
		public LineInfo(byte token, Object obj)
		{
			this.token = token;
			this.obj = obj;
		}

		/**
		 * The id of the last token of the line.
		 */
		public byte token;

		/**
		 * This is for use by the token marker implementations
		 * themselves. It can be used to store anything that
		 * is an object and that needs to exist on a per-line
		 * basis.
		 */
		public Object obj;
	}
}
