/******************************************************************************
 JTEStyler.cc

	This is the base class for an object that can efficiently control the
	styles inside a JTextEditor.  Derive a class from JTextEditor and
	override AdjustStylesBeforeRecalc() and pass the arguments directly to
	JTEStyler::UpdateStyles().

	tokenStartList is used to avoid lexing more of the text than necessary.
	Each JTextEditor object must own one and must never modify the contents
	between calls to UpdateStyles().

	Derived classes must override the following function:

		Scan
			Read tokens from the given input stream and call SetFont() for
			each one.  When SetFont() returns kFalse, stop scanning.

			Every character in the input stream must be part of some token,
			and the tokens must be passed to SetFont() in the order that they
			occur in the file.

			Every time the lexer reaches a point where it could be safely
			restarted, call SaveTokenStart().  (This cannot be done for every
			token because, for example, '*' can mean either multiplication or
			dereferencing in C, so one must start further back in order to
			get the context.)

	Derived classes can override the following function:

		PreexpandCheckRange
			Called before Scan() to allow the derived class to expand the
			range of characters that need to be checked.  The initial range
			starts with the first character in the style run containing the
			first modified character and ends with the last character in the
			style run containing the last modified character.  These points
			are obviously token boundaries.  In some cases, one needs to check
			a wider range.

	While inside Scan(), one may encounter a token that requires checking
	at least a certain distance (e.g. to the end of the line).  To handle
	this, call ExtendCheckRange() with the index of the last character that
	must be checked.  This should be done before calling SetFont() for the
	token.

	BASE CLASS = none

	Copyright © 1998 by John Lindal. All rights reserved.

 ******************************************************************************/

#include <JTEStyler.h>
#include <JFontManager.h>
#include <JOrderedSetUtil.h>
#include <JMinMax.h>
#include <jStreamUtil.h>
#include <jStrStreamUtil.h>
#include <JStopWatch.h>
#include <jAssert.h>

const JSize kDecimationFactor = 50;
const JSize kListBlockSize    = 50;

#define DEBUG_RUN_INFO		0	// boolean
#define DEBUG_TIMING_INFO	0	// boolean

#define run_assert(styles, index, runIndex, firstIndexInRun) \
	assert( check_run(styles, index, runIndex, firstIndexInRun) )

#if DEBUG_RUN_INFO

JIndex debugRunIndex, debugFirstInRun;

#define check_run(styles, index, runIndex, firstIndexInRun) \
	styles->FindRun(index, &debugRunIndex, &debugFirstInRun) && \
	debugRunIndex == runIndex && debugFirstInRun == firstIndexInRun

#else

#define check_run(styles, index, runIndex, firstIndexInRun)		kTrue

#endif

/******************************************************************************
 Constructor

 ******************************************************************************/

JTEStyler::JTEStyler()
{
	itsActiveFlag  = kTrue;

	itsTE          = NULL;
	itsFontMgr     = NULL;
	itsText        = NULL;
	itsStyles      = NULL;
	itsRecalcRange = NULL;
	itsRedrawRange = NULL;

	itsTokenStartList   = NULL;
	itsTokenStartFactor = kDecimationFactor;
}

/******************************************************************************
 Destructor

 ******************************************************************************/

JTEStyler::~JTEStyler()
{
}

/******************************************************************************
 UpdateStyles

	*range is inside [1, text.GetLength()+1]

 ******************************************************************************/

void
JTEStyler::UpdateStyles
	(
	const JTextEditor*				te,
	const JString&					text,
	JRunArray<JTextEditor::Font>*	styles,
	JIndexRange*					recalcRange,
	JIndexRange*					redrawRange,
	const JBoolean					deletion,
	JArray<JIndex>*					tokenStartList
	)
{
	if (!itsActiveFlag)
		{
		tokenStartList->RemoveAll();
		return;
		}

	const JSize textLength = text.GetLength();
	if (textLength == 0)
		{
		tokenStartList->RemoveAll();
		return;
		}

	tokenStartList->SetSortOrder(JOrderedSetT::kSortAscending);
	tokenStartList->SetCompareFunction(JCompareIndices);

	te->GetDefaultFont(&itsDefFontID, &itsFontSize, &itsDefFontStyle);
	itsFontName = te->GetDefaultFontName();

	if (recalcRange->first == 1 && recalcRange->last >= text.GetLength())
		{
		itsRedoAllFlag = kTrue;
		itsCheckRange.Set(1, text.GetLength());
		tokenStartList->RemoveAll();
		tokenStartList->AppendElement(1);
		itsTokenStart = 1;

		styles->RemoveAll();
		}
	else
		{
		itsRedoAllFlag = kFalse;

		// calculate the range that needs to be checked

		JIndex firstIndex = recalcRange->first;
		JIndex lastIndex  = recalcRange->last;
		if ((deletion && firstIndex > 1) || firstIndex > textLength)
			{
			// This fixes the case when the last character of the token is deleted.

			firstIndex--;
			}
		if (lastIndex > textLength)
			{
			// We can't decrease recalcRange's end index, and we can't find
			// textLength+1 in *styles.

			lastIndex = textLength;
			}

		JIndex runIndex1, firstIndexInRun1;
		JBoolean ok = styles->FindRun(firstIndex, &runIndex1, &firstIndexInRun1);
		assert( ok );

		JIndex runIndex2        = runIndex1;
		JIndex firstIndexInRun2 = firstIndexInRun1;
		ok = styles->FindRun(firstIndex, lastIndex, &runIndex2, &firstIndexInRun2);
		assert( ok );
		run_assert(styles, lastIndex, runIndex2, firstIndexInRun2);

		itsCheckRange.Set(firstIndexInRun1, firstIndexInRun2 + styles->GetRunLength(runIndex2)-1);

		// let derived class expand the range

		JIndexRange savedRange = itsCheckRange;
		PreexpandCheckRange(text, *styles, *recalcRange, deletion, &itsCheckRange);
		assert( itsCheckRange.Contains(savedRange) &&
				itsCheckRange.last <= styles->GetElementCount() );

		// find nearest token in front of itsCheckRange

		if (tokenStartList->IsEmpty())
			{
			tokenStartList->AppendElement(1);
			}

		JBoolean foundTokenStart;
		JIndex tokenStartIndex =
			tokenStartList->SearchSorted1(itsCheckRange.first, JOrderedSetT::kAnyMatch, &foundTokenStart);
		if (!foundTokenStart)
			{
			tokenStartIndex--;	// wants to insert -after- the value
			}

		// While typing in one place, it is much faster to back up from itsCheckRange
		// than to start from the top.

		itsTokenStart      = tokenStartList->GetElement(tokenStartIndex);
		itsTokenRunIndex   = runIndex1;
		itsTokenFirstInRun = firstIndexInRun1;
		ok = styles->FindRun(firstIndex, itsTokenStart, &itsTokenRunIndex, &itsTokenFirstInRun);
		assert( ok );
		run_assert(styles, itsTokenStart, itsTokenRunIndex, itsTokenFirstInRun);

		// the rest of the token starts are invalid

		const JSize tokenStartCount = tokenStartList->GetElementCount();
		if (tokenStartIndex < tokenStartCount)
			{
			tokenStartList->RemoveNextElements(tokenStartIndex+1, tokenStartCount - tokenStartIndex);
			}
		}

	// prepare to accumulate new token starts

	itsTokenStartList  = tokenStartList;
	itsTokenStartCount = 0;

	// scan the text and adjust the styles

	jistrstream(input, text.GetCString(), text.GetLength());
	JSeekg(input, itsTokenStart-1);

	itsTE          = te;
	itsFontMgr     = te->TEGetFontManager();
	itsText        = &text;
	itsStyles      = styles;
	itsRecalcRange = recalcRange;
	itsRedrawRange = redrawRange;

	#if DEBUG_TIMING_INFO
	JStopWatch timer;
	timer.StartTimer();
	#endif

	Scan(input);

	#if DEBUG_TIMING_INFO
	timer.StopTimer();
	cout << "JTEStyler: " << timer.PrintTimeInterval() << endl;
	#endif

	itsTE             = NULL;
	itsFontMgr        = NULL;
	itsText           = NULL;
	itsStyles         = NULL;
	itsRecalcRange    = NULL;
	itsRedrawRange    = NULL;
	itsTokenStartList = NULL;
}

/******************************************************************************
 PreexpandCheckRange (virtual protected)

	*checkRange is only allowed to expand.  The default is to do nothing.

	modifiedRange is the range of text that was changed.
	deletion is kTrue if the modification was that text was deleted.

 ******************************************************************************/

void
JTEStyler::PreexpandCheckRange
	(
	const JString&						text,
	const JRunArray<JTextEditor::Font>&	styles,
	const JIndexRange&					modifiedRange,
	const JBoolean						deletion,
	JIndexRange*						checkRange
	)
{
}

/******************************************************************************
 ExtendCheckRange (protected)

	Extends the end of the check range.  It is safe to call this with
	an index beyond the end of the text.

 ******************************************************************************/

void
JTEStyler::ExtendCheckRange
	(
	const JIndex newEndIndex
	)
{
	if (itsCheckRange.last < newEndIndex)
		{
		itsCheckRange.last = JMin(newEndIndex, itsText->GetLength());
		}
}

/******************************************************************************
 SetFont (protected)

	Returns kTrue if the lexer should keep going.

 ******************************************************************************/

JBoolean
JTEStyler::SetFont
	(
	const JIndexRange&	range,
	const JFontStyle&	style
	)
{
	assert( !range.IsEmpty() );

	if (itsCheckRange.last < range.first)
		{
		// we are beyond the range where anything could have changed
		return kFalse;
		}

	assert( range.first == itsTokenStart );

	if (itsRedoAllFlag)
		{
		JFontID fid = itsDefFontID;
		if (!OnlyColorChanged(style, itsDefFontStyle))
			{
			fid = itsFontMgr->GetFontID(itsFontName, itsFontSize, style);
			}
		itsStyles->AppendElements(JTextEditor::Font(fid, itsFontSize, style),
								  range.GetLength());
		}
	else if (range.last >= itsCheckRange.first)
		{
		JIndexRange fontRange;
		fontRange.SetFirstAndLength(itsTokenFirstInRun, itsStyles->GetRunLength(itsTokenRunIndex));
		const JBoolean beyondCurrentRun = JNegate( fontRange.Contains(range) );

		JTextEditor::Font f = itsStyles->GetRunData(itsTokenRunIndex);
		if (beyondCurrentRun || style != f.style)
			{
			// extend the check range if we slop over into another style run
			// (HTML: type '<' after 'x' in "x<!--<br><h3>text</h3>-->")

			if (beyondCurrentRun)
				{
				JIndex runIndex   = itsTokenRunIndex;
				JIndex firstInRun = itsTokenFirstInRun;
				const JBoolean ok = itsStyles->FindRun(itsTokenStart, range.last,
													   &runIndex, &firstInRun);
				ExtendCheckRange(firstInRun + itsStyles->GetRunLength(runIndex)-1);
				}

			// update the styles

			if (!beyondCurrentRun && OnlyColorChanged(style, f.style))
				{
				*itsRedrawRange += range;
				}
			else
				{
				*itsRecalcRange += range;
				}

			f.size  = itsFontSize;
			f.style = style;
			if (OnlyColorChanged(style, itsDefFontStyle))
				{
				f.id = itsDefFontID;
				}
			else
				{
				f.id = itsFontMgr->GetFontID(itsFontName, itsFontSize, style);
				}

			itsStyles->RemoveNextElements(range.first, range.GetLength(),
										  &itsTokenRunIndex, &itsTokenFirstInRun);

			run_assert(itsStyles, range.first, itsTokenRunIndex, itsTokenFirstInRun);

			itsStyles->InsertElementsAtIndex(range.first, f, range.GetLength(),
											 &itsTokenRunIndex, &itsTokenFirstInRun);

			run_assert(itsStyles, range.first, itsTokenRunIndex, itsTokenFirstInRun);
			}
		}

	itsTokenStart = range.last+1;
	if (!itsRedoAllFlag)
		{
		const JBoolean ok = itsStyles->FindRun(range.first, itsTokenStart,
											   &itsTokenRunIndex, &itsTokenFirstInRun);
		assert( ok || range.last == itsText->GetLength() );
		assert( range.last == itsText->GetLength() ||
				check_run(itsStyles, itsTokenStart, itsTokenRunIndex, itsTokenFirstInRun) );
		}

	return JConvertToBoolean( range.last < itsCheckRange.last );
}

/******************************************************************************
 NewTokenStartList (static)

	Allocates a tokenStartList with a reasonable block size.

 ******************************************************************************/

JArray<JIndex>*
JTEStyler::NewTokenStartList()
{
	JArray<JIndex>* list = new JArray<JIndex>(kListBlockSize);
	assert( list != NULL );
	return list;
}

/******************************************************************************
 SaveTokenStart (protected)

	Automatically decimates the stream of token starts by itsTokenStartFactor.

 ******************************************************************************/

void
JTEStyler::SaveTokenStart()
{
	itsTokenStartCount++;
	if (itsTokenStartCount >= itsTokenStartFactor)
		{
		itsTokenStartCount = 0;
		itsTokenStartList->AppendElement(itsTokenStart);
		}
}
