/*
 * DisplayManager.java - Low-level text display
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 2001, 2005 Slava Pestov
 *
 * 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 java.awt.Toolkit;
import java.util.*;
import org.gjt.sp.jedit.buffer.*;
import org.gjt.sp.jedit.Debug;
import org.gjt.sp.util.Log;
//}}}

/**
 * Manages low-level text display tasks, such as folding.
 * 
 * @since jEdit 4.2pre1
 * @author Slava Pestov
 * @version $Id: DisplayManager.java 22670 2013-01-12 12:29:48Z thomasmey $
 */
public class DisplayManager
{
	//{{{ Static part

	//{{{ getDisplayManager() method
	static DisplayManager getDisplayManager(JEditBuffer buffer,
		TextArea textArea)
	{
		List<DisplayManager> l = bufferMap.get(buffer);
		if(l == null)
		{
			l = new LinkedList<DisplayManager>();
			bufferMap.put(buffer,l);
		}

		/* An existing display manager's fold visibility map
		that a new display manager will inherit */
		DisplayManager copy = null;
		Iterator<DisplayManager> liter = l.iterator();
		DisplayManager dmgr;
		while(liter.hasNext())
		{
			dmgr = liter.next();
			copy = dmgr;
			if(!dmgr.inUse && dmgr.textArea == textArea)
			{
				dmgr.inUse = true;
				return dmgr;
			}
		}

		// if we got here, no unused display manager in list
		dmgr = new DisplayManager(buffer,textArea,copy);
		dmgr.inUse = true;
		l.add(dmgr);

		return dmgr;
	} //}}}

	//{{{ release() method
	void release()
	{
		inUse = false;
	} //}}}

	//{{{ bufferClosed() method
	public static void bufferClosed(JEditBuffer buffer)
	{
		bufferMap.remove(buffer);
	} //}}}

	//{{{ textAreaDisposed() method
	static void textAreaDisposed(TextArea textArea)
	{
		for (List<DisplayManager> l : bufferMap.values())
		{
			Iterator<DisplayManager> liter = l.iterator();
			while(liter.hasNext())
			{
				DisplayManager dmgr = liter.next();
				if(dmgr.textArea == textArea)
				{
					dmgr.dispose();
					liter.remove();
				}
			}
		}
	} //}}}

	private static final Map<JEditBuffer, List<DisplayManager>> bufferMap = new HashMap<JEditBuffer, List<DisplayManager>>();
	//}}}

	//{{{ getBuffer() method
	/**
	 * @since jEdit 4.3pre3
	 */
	public JEditBuffer getBuffer()
	{
		return buffer;
	} //}}}

	//{{{ isLineVisible() method
	/**
	 * Returns if the specified physical line is visible.
	 * @param line A physical line index
	 * @since jEdit 4.2pre1
	 */
	public final boolean isLineVisible(int line)
	{
		return folds.search(line) % 2 == 0;
	} //}}}

	//{{{ isOutsideNarrowing() method
	/**
	 * Returns true if the display is narrowed and the specified line is
	 * outside of the narrowing.
	 * @param line A physical line index
	 * @since jEdit 4.5
	 */
	public boolean isOutsideNarrowing(int line)
	{
		if (line < getFirstVisibleLine())
			return true;
		
		// If the line is beyond the last visible line, it may still be within the narrowed
		// display if the last visible line is a root for the fold containing the given
		// line.
		if (line > getLastVisibleLine())
		{
			int lastVisible = getLastVisibleLine();
			int lastVisibleLevel = buffer.getFoldLevel(lastVisible);
			if (buffer.getFoldLevel(line) <= lastVisibleLevel)
				return true;
			
			// Any line between the last visible and the given line with a fold level
			// <= that of the last visible will break the root-child relationship
			for (int i = lastVisible + 1; i <= line; i++)
			{
				if (buffer.getFoldLevel(i) <= lastVisibleLevel)
					return true;
			}
			
			// If we get to this point, it means that the last visible line is a
			// fold-level root for the given line
			return false;
		}
		
		return false;
	} //}}}
	
	//{{{ getFirstVisibleLine() method
	/**
	 * Returns the physical line number of the first visible line.
	 * @since jEdit 4.2pre1
	 */
	public int getFirstVisibleLine()
	{
		return folds.first();
	} //}}}

	//{{{ getLastVisibleLine() method
	/**
	 * Returns the physical line number of the last visible line.
	 * @since jEdit 4.2pre1
	 */
	public int getLastVisibleLine()
	{
		return folds.last();
	} //}}}

	//{{{ getNextVisibleLine() method
	/**
	 * Returns the next visible line after the specified line index,
	 * or (-1) if there is no next visible line.
	 * @param line A physical line index
	 * @since jEdit 4.0pre1
	 */
	public int getNextVisibleLine(int line)
	{
		if(line < 0 || line >= buffer.getLineCount())
			throw new ArrayIndexOutOfBoundsException(line);

		return folds.next(line);
	} //}}}

	//{{{ getPrevVisibleLine() method
	/**
	 * Returns the previous visible line before the specified line index.
	 * @param line a physical line index
	 * @return the previous visible physical line or -1 if there is no visible line
	 * @since jEdit 4.0pre1
	 */
	public int getPrevVisibleLine(int line)
	{
		if(line < 0 || line >= buffer.getLineCount())
			throw new ArrayIndexOutOfBoundsException(line);

		return folds.prev(line);
	} //}}}

	//{{{ getScreenLineCount() method
	/**
	 * Returns how many screen lines contains the given physical line.
	 * It can be greater than 1 when using soft wrap
	 *
	 * @param line the physical line
	 * @return the screen line count
	 */
	public final int getScreenLineCount(int line)
	{
		updateScreenLineCount(line);
		return screenLineMgr.getScreenLineCount(line);
	} //}}}

	//{{{ getScrollLineCount() method
	/**
	 * Returns the number of displayable lines
	 * It can be greater than the number of lines of the buffer when using
	 * soft wrap (a line can count for n lines), or when using folding, if
	 * the foldings are collapsed
	 * @return the number of displayable lines
	 */
	public final int getScrollLineCount()
	{
		return scrollLineCount.getScrollLine();
	} //}}}

	//{{{ collapseFold() method
	/**
	 * Collapses the fold at the specified physical line index.
	 * @param line A physical line index
	 * @since jEdit 4.2pre1
	 */
	public void collapseFold(int line)
	{
		int lineCount = buffer.getLineCount();
		int end = lineCount - 1;

		// if the caret is on a collapsed fold, collapse the
		// parent fold
		if(line != 0
			&& line != lineCount - 1
			&& buffer.isFoldStart(line)
			&& !isLineVisible(line + 1))
		{
			line--;
		}

		int initialFoldLevel = buffer.getFoldLevel(line);

		//{{{ Find fold start and end...
		int start = 0;
		if(line != lineCount - 1
			&& buffer.getFoldLevel(line + 1) > initialFoldLevel)
		{
			// this line is the start of a fold
			start = line + 1;

			for(int i = line + 1; i < lineCount; i++)
			{
				if(buffer.getFoldLevel(i) <= initialFoldLevel)
				{
					end = i - 1;
					break;
				}
			}
		}
		else
		{
			boolean ok = false;

			// scan backwards looking for the start
			for(int i = line - 1; i >= 0; i--)
			{
				if(buffer.getFoldLevel(i) < initialFoldLevel)
				{
					start = i + 1;
					ok = true;
					break;
				}
			}

			if(!ok)
			{
				// no folds in buffer
				return;
			}

			for(int i = line + 1; i < lineCount; i++)
			{
				if(buffer.getFoldLevel(i) < initialFoldLevel)
				{
					end = i - 1;
					break;
				}
			}
		} //}}}

		// Collapse the fold...
		hideLineRange(start,end);

		notifyScreenLineChanges();
		textArea.foldStructureChanged();
	} //}}}

	//{{{ expandFold() method
	/**
	 * Expands the fold at the specified physical line index.
	 * @param line A physical line index
	 * @param fully If true, all subfolds will also be expanded
	 * @return the line number of the first subfold, or -1 if none
	 * @since jEdit 4.2pre1
	 */
	public int expandFold(int line, boolean fully)
	{
		MutableInteger firstSubfold = new MutableInteger(-1);
		boolean unfolded = _expandFold(line, fully, firstSubfold);
		
		if (unfolded)
			textArea.foldStructureChanged();
		
		return firstSubfold.get();
	} //}}}

	//{{{ expandAllFolds() method
	/**
	 * Expands all folds.
	 * @since jEdit 4.2pre1
	 */
	public void expandAllFolds()
	{
		showLineRange(0,buffer.getLineCount() - 1);
		notifyScreenLineChanges();
		textArea.foldStructureChanged();
	} //}}}

	//{{{ expandFolds() method
	/**
	 * This method should only be called from <code>actions.xml</code>.
	 * @since jEdit 4.2pre1
	 */
	public void expandFolds(char digit)
	{
		if(digit < '1' || digit > '9')
		{
			Toolkit.getDefaultToolkit().beep();
		}
		else
			expandFolds((digit - '1') + 1);
	} //}}}

	//{{{ expandFolds() method
	/**
	 * Expands all folds with the specified fold level.
	 * @param foldLevel The fold level
	 * @param update If true, notify the text area of a fold level change. Since this will
	 *   automatically move the caret if still inside a fold, this may not be what we want.
	 * @since jEdit 4.5
	 */
	public void expandFolds(int foldLevel, boolean update)
	{
		if(buffer.getFoldHandler() instanceof IndentFoldHandler)
			foldLevel = (foldLevel - 1) * buffer.getIndentSize() + 1;

		int lineCount = buffer.getLineCount();
		int end = lineCount - 1;
		showLineRange(0,end);

		int leastFolded = -1;
		int firstInvisible = 0;

		for(int i = 0; i < lineCount; i++)
		{
			int level = buffer.getFoldLevel(i);
			// Keep track of the least fold level up to this point in the file,
			// because we can't hide a line at this level since there will be no "root"
			// line to unfold it from
			if (leastFolded == -1 || level < leastFolded)
			{
				leastFolded = level;
			}
		
			if (level < foldLevel || level == leastFolded)
			{
				if(firstInvisible != i)
				{
					hideLineRange(firstInvisible,
						i - 1);
				}
				firstInvisible = i + 1;
			}
		}

		if(firstInvisible != lineCount)
			hideLineRange(firstInvisible,end);

		notifyScreenLineChanges();
		if(update && textArea.getDisplayManager() == this)
		{
			textArea.foldStructureChanged();
		}
	} //}}}
	
	//{{{ expandFolds() method
	/**
	 * Expands all folds with the specified fold level.
	 * @param foldLevel The fold level
	 * @since jEdit 4.2pre1
	 */
	public void expandFolds(int foldLevel)
	{
		expandFolds(foldLevel, true);
	} //}}}

	//{{{ narrow() method
	/**
	 * Narrows the visible portion of the buffer to the specified
	 * line range.
	 * @param start The first line
	 * @param end The last line
	 * @since jEdit 4.2pre1
	 */
	public void narrow(int start, int end)
	{
		int lineCount = buffer.getLineCount();
		if(start > end || start < 0 || end >= lineCount)
			throw new ArrayIndexOutOfBoundsException(start + ", " + end);

		if(start < getFirstVisibleLine() || end > getLastVisibleLine())
			expandAllFolds();

		if(start != 0)
			hideLineRange(0,start - 1);
		if(end != lineCount - 1)
			hideLineRange(end + 1,lineCount - 1);

		// if we narrowed to a single collapsed fold
		if(start != lineCount - 1 && !isLineVisible(start + 1))
			expandFold(start,false);

		textArea.fireNarrowActive();

		notifyScreenLineChanges();
		textArea.foldStructureChanged();
	} //}}}

	//{{{ Package-private members
	final FirstLine firstLine;
	final ScrollLineCount scrollLineCount;
	final ScreenLineManager screenLineMgr;
	final RangeMap folds;

	//{{{ init() method
	void init()
	{
		// Needs information available in textArea only when a
		// DisplayManager is active in it.
		assert textArea.getDisplayManager() == this;

		if(buffer.isLoading())
			// init() will be called later from bufferLoaded().
			return;

		if(!initialized)
		{
			folds.reset(buffer.getLineCount());
			resetAnchors();
			int collapseFolds = buffer.getIntegerProperty(
				"collapseFolds",0);
			if(collapseFolds != 0)
				expandFolds(collapseFolds);
			initialized = true;
		}
		else
		{
			// Already initialized.
			// Just make the scroll bar updated.
			resetAnchors();
		}
	} //}}}

	//{{{ notifyScreenLineChanges() method
	/**
	 * FirstLine or ScrollLineCount changed
	 * Update ScrollBar, etc.
	 */
	void notifyScreenLineChanges()
	{
		if(Debug.SCROLL_DEBUG)
			Log.log(Log.DEBUG,this,"notifyScreenLineChanges()");

		// Screen line change must be issued when the textArea
		// has information of wrap mode for the buffer.
		// Otherwise, the screen line calculation will be incorrect.
		assert textArea.getDisplayManager() == this;

		try
		{
			if(firstLine.isCallReset())
				firstLine.reset();
			else if(firstLine.isCallChanged())
				firstLine.changed();

			if(scrollLineCount.isCallReset())
			{
				scrollLineCount.reset();
				//FIXME: Why here?
				firstLine.ensurePhysicalLineIsVisible();
			} else if(scrollLineCount.isCallChanged())
				scrollLineCount.changed();

			if(firstLine.isCallChanged() ||
			   scrollLineCount.isCallReset() ||
			   scrollLineCount.isCallChanged())
			{
				textArea.updateScrollBar();
				textArea.recalculateLastPhysicalLine();
			}
		}
		finally
		{
			firstLine.resetCallState();
			scrollLineCount.resetCallState();
		}
	} //}}}

	//{{{ setFirstLine() method
	/**
	 * Sets the vertical scroll bar position
	 *
	 * @param currentFirstLine the current scroll bar position
	 * @param newFirstLine The to-be scroll bar position
	 */
	void setFirstLine(int currentFirstLine, int newFirstLine)
	{
		int visibleLines = textArea.getVisibleLines();

		if(newFirstLine >= currentFirstLine + visibleLines)
		{
			this.firstLine.scrollDown(newFirstLine - currentFirstLine);
			textArea.chunkCache.invalidateAll();
		}
		else if(newFirstLine <= currentFirstLine - visibleLines)
		{
			this.firstLine.scrollUp(currentFirstLine - newFirstLine);
			textArea.chunkCache.invalidateAll();
		}
		else if(newFirstLine > currentFirstLine)
		{
			this.firstLine.scrollDown(newFirstLine - currentFirstLine);
			textArea.chunkCache.scrollDown(newFirstLine - currentFirstLine);
		}
		else if(newFirstLine < currentFirstLine)
		{
			this.firstLine.scrollUp(currentFirstLine - newFirstLine);
			textArea.chunkCache.scrollUp(currentFirstLine - newFirstLine);
		} else
			assert true;

		notifyScreenLineChanges();
	} //}}}

	//{{{ setFirstPhysicalLine() method
	/**
	 * Scroll from a given amount of lines.
	 *
	 * @param amount the amount of lines that must be scrolled
	 * @param skew a skew within the given line
	 */
	void setFirstPhysicalLine(int amount, int skew)
	{
		int currentFirstLine = textArea.getFirstLine();

		if(amount == 0)
		{
			skew -= this.firstLine.getSkew();

			// JEditTextArea.scrollTo() needs this to simplify
			// its code
			if(skew < 0)
				this.firstLine.scrollUp(-skew);
			else if(skew > 0)
				this.firstLine.scrollDown(skew);
			else
			{
				// nothing to do
				return;
			}
		}
		else if(amount > 0)
			this.firstLine.physDown(amount,skew);
		else if(amount < 0)
			this.firstLine.physUp(-amount,skew);

		int firstLine = textArea.getFirstLine();
		int visibleLines = textArea.getVisibleLines();

		if(firstLine == currentFirstLine)
			/* do nothing */;
		else if(firstLine >= currentFirstLine + visibleLines
			|| firstLine <= currentFirstLine - visibleLines)
		{
			textArea.chunkCache.invalidateAll();
		}
		else if(firstLine > currentFirstLine)
		{
			textArea.chunkCache.scrollDown(firstLine - currentFirstLine);
		}
		else if(firstLine < currentFirstLine)
		{
			textArea.chunkCache.scrollUp(currentFirstLine - firstLine);
		}

		// we have to be careful
		notifyScreenLineChanges();
	} //}}}

	//{{{ invalidateScreenLineCounts() method
	void invalidateScreenLineCounts()
	{
		screenLineMgr.invalidateScreenLineCounts();
		firstLine.setCallReset(true);
		scrollLineCount.setCallReset(true);
	} //}}}

	//{{{ updateScreenLineCount() method
	void updateScreenLineCount(int line)
	{
		// If this DisplayManager is not current visible one,
		// screen line count can't be determined since the wrap
		// mode and the wrap margin (and chunkCache) for the
		// buffer are not available.
		// Maybe, those information should be in DisplayManager
		// instead of textArea.
		assert textArea.getDisplayManager() == this;

		if(!screenLineMgr.isScreenLineCountValid(line))
		{
			// reset chunk cache here
			textArea.chunkCache.reset();
			int newCount = textArea.chunkCache.getLineSubregionCount(line);

			assert newCount > 0;
			setScreenLineCount(line,newCount);
		}
	} //}}}

	//{{{ bufferLoaded() method
	void bufferLoaded()
	{
		initialized = false;
		folds.reset(buffer.getLineCount());
		screenLineMgr.reset();
		if(textArea.getDisplayManager() == this)
		{
			textArea.propertiesChanged();
			init();
		}
		else
		{
			// init() will be called later when the buffer
			// is set in the textArea.
		}
	} //}}}

	//{{{ foldHandlerChanged() method
	void foldHandlerChanged()
	{
		if(buffer.isLoading())
			// Happens once before bufferLoaded() is called.
			// It seems violating the javadoc on BufferListener,
			// but it's not a problem in DisplayManager because
			// this is called later on init(), possibly via
			// bufferLoaded().
			return;

		initialized = false;
		folds.reset(buffer.getLineCount());
		if(textArea.getDisplayManager() == this)
		{
			init();
		}
		else
		{
			// init() will be called later when the buffer
			// is set in the textArea.
		}
	} //}}}

	//}}}

	//{{{ Private members
	private boolean initialized;
	private boolean inUse;
	private final JEditBuffer buffer;
	private final TextArea textArea;
	private final BufferHandler bufferHandler;
	private final ElasticTabStopBufferListener elasticTabStopListener;

	//{{{ DisplayManager constructor
	private DisplayManager(JEditBuffer buffer, TextArea textArea,
		DisplayManager copy)
	{
		this.buffer = buffer;
		this.screenLineMgr = new ScreenLineManager(buffer);
		this.textArea = textArea;

		scrollLineCount = new ScrollLineCount(this,textArea);
		firstLine = new FirstLine(this,textArea);
		bufferHandler = new BufferHandler(this,textArea,buffer);
		//TODO:invoke ElasticTabStopBufferListener methods from inside BufferHandler to avoid chunking same line twice
		elasticTabStopListener = new ElasticTabStopBufferListener(textArea);
		buffer.addBufferListener(elasticTabStopListener, JEditBuffer.HIGH_PRIORITY);
		// this listener priority thing is a bad hack...
		buffer.addBufferListener(bufferHandler, JEditBuffer.HIGH_PRIORITY);

		if(copy != null)
		{
			folds = new RangeMap(copy.folds);
			initialized = true;
		}
		else
		{
			folds = new RangeMap();
			folds.reset(0);
		}
	} //}}}

	//{{{ resetAnchors() method
	private void resetAnchors()
	{
		firstLine.setCallReset(true);
		scrollLineCount.setCallReset(true);
		notifyScreenLineChanges();
	} //}}}

	//{{{ dispose() method
	private void dispose()
	{
		buffer.removeBufferListener(bufferHandler);
		buffer.removeBufferListener(elasticTabStopListener);
	} //}}}

	//{{{ showLineRange() method
	// for folding
	private void showLineRange(int start, int end)
	{
		if(Debug.FOLD_VIS_DEBUG)
		{
			Log.log(Log.DEBUG,this,"showLineRange(" + start
				+ ',' + end + ')');
		}

		for(int i = start; i <= end; i++)
		{
			//XXX
			if(!isLineVisible(i))
			{
				// important: not screenLineMgr.getScreenLineCount()
				int screenLines = getScreenLineCount(i);
				if(firstLine.getPhysicalLine() >= i)
				{
					firstLine.moveScrollLine(screenLines);
				}
				scrollLineCount.moveScrollLine(screenLines);
			}
		}

		/* update fold visibility map. */
		folds.show(start,end);
	} //}}}

	//{{{ hideLineRange() method
	private void hideLineRange(int start, int end)
	{
		if(Debug.FOLD_VIS_DEBUG)
		{
			Log.log(Log.DEBUG,this,"hideLineRange(" + start
				+ ',' + end + ')');
		}

		int physicalLine = start;
		if(!isLineVisible(physicalLine))
			physicalLine = getNextVisibleLine(physicalLine);

		int scrollLines = 0;
		while(physicalLine != -1 && physicalLine <= end)
		{
			int screenLines = getScreenLineCount(physicalLine);
			if(physicalLine < firstLine.getPhysicalLine())
			{
				firstLine.setSkew(0);
				firstLine.moveScrollLine(-screenLines);
			}

			scrollLines -= screenLines;
			physicalLine = getNextVisibleLine(physicalLine);
		}
		scrollLineCount.moveScrollLine(scrollLines);

		/* update fold visibility map. */
		folds.hide(start,end);

		if(!isLineVisible(firstLine.getPhysicalLine()))
		{
			int firstVisible = getFirstVisibleLine();
			if(firstLine.getPhysicalLine() < firstVisible)
			{
				firstLine.setPhysicalLine(firstVisible);
				firstLine.setScrollLine(0);
			}
			else
			{
				firstLine.setPhysicalLine(getPrevVisibleLine(firstLine.getPhysicalLine()));
				firstLine.moveScrollLine(-getScreenLineCount(firstLine.getPhysicalLine()));
			}
		}
	} //}}}

	//{{{ setScreenLineCount() method
	/**
	 * Sets the number of screen lines that the specified physical line
	 * is split into.
	 * @param line the physical line number
	 * @param count the line count (== 1 if no wrap, > 1 if soft wrap)
	 * @since jEdit 4.2pre1
	 */
	private void setScreenLineCount(int line, int count)
	{
		assert count > 0;
		screenLineMgr.setScreenLineCount(line,count);

	} //}}}

	//{{{ _expandFold() method
	/**
	 * Expands the fold at the specified physical line index.
	 * @param line A physical line index
	 * @param fully If true, all subfolds will also be expanded
	 * @param firstSubfold Will be set to the line number of the first
	 * subfold, or -1 if there is none.
	 * @return True if some line was unfolded, false otherwise.
	 */
	private boolean _expandFold(int line, boolean fully, MutableInteger firstSubfold)
	{
		boolean unfolded = false;

		int lineCount = buffer.getLineCount();
		int end = lineCount - 1;

		while (!isLineVisible(line))
		{
			int prevLine = folds.lookup(folds.search(line)) - 1;
			if (!isLineVisible(prevLine))
			{
				return unfolded;
			}
			
			// If any fold farther up was unfolded, then the text
			// area needs to be updated
			unfolded |= _expandFold(prevLine, fully, firstSubfold);
			
			if (!isLineVisible(prevLine + 1))
			{
				return unfolded;
			}
		}
		
		// At this point, line is already visible, but if the fully flag is set, we may
		// still need to look for and show its subfolds.
		if (line == (lineCount - 1) || (isLineVisible(line + 1) && !fully))
		{
			return unfolded;
		}

		//{{{ Find fold start and fold end...
		int start;
		int initialFoldLevel = buffer.getFoldLevel(line);
		if (buffer.getFoldLevel(line + 1) > initialFoldLevel)
		{
			// this line is the start of a fold
			start = line;
			if (!isLineVisible(line + 1) && folds.search(line + 1) != folds.count() - 1)
			{
				int index = folds.search(line + 1);
				end = folds.lookup(index + 1) - 1;
			}
			else
			{
				for (int i = line + 1; i < lineCount; i++)
				{
					if (buffer.getFoldLevel(i) <= initialFoldLevel)
					{
						end = i - 1;
						break;
					}
				}
			}
		}
		else
		{
			if (!fully)
			{
				return unfolded;
			}
			start = line;
			while (start > 0 && buffer.getFoldLevel(start) >= initialFoldLevel)
			{
				start--;
			}
			initialFoldLevel = buffer.getFoldLevel(start);
			for (int i = line + 1; i < lineCount; i++)
			{
				if (buffer.getFoldLevel(i) <= initialFoldLevel)
				{
					end = i - 1;
					break;
				}
			}
		} // }}}

		//{{{ Expand the fold...
		if(fully)
		{
			showLineRange(start,end);
		}
		else
		{
			boolean foundSubfold = false;
			for (int i = start + 1; i <= end;)
			{
				if (!foundSubfold && buffer.isFoldStart(i))
				{
					firstSubfold.set(i);
					foundSubfold = true;
				}

				showLineRange(i, i);
				int fold = buffer.getFoldLevel(i);
				i++;
				while (i <= end && buffer.getFoldLevel(i) > fold)
				{
					i++;
				}
			}
		}
		
		unfolded = true;
		// }}}
		
		notifyScreenLineChanges();
		return unfolded;
	} //}}}
	
	//{{{ MutableInteger class
	private class MutableInteger
	{
		MutableInteger(int value)
		{
			this.value = value;
		}
		
		public void set(int value)
		{
			this.value = value;
		}
		
		public int get()
		{
			return value;
		}
		
		private int value;
	} //}}}
	
	//}}}
}
