/*
 * 10/08/2011
 *
 * FoldManager.java - Manages code folding in an RSyntaxTextArea instance.
 * 
 * This library is distributed under a modified BSD license.  See the included
 * RSyntaxTextArea.License.txt file for details.
 */
package com.fr.design.gui.syntax.ui.rsyntaxtextarea.folding;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;

import com.fr.design.gui.syntax.ui.rsyntaxtextarea.RSyntaxDocument;
import com.fr.design.gui.syntax.ui.rsyntaxtextarea.RSyntaxTextArea;
import com.fr.design.gui.syntax.ui.rsyntaxtextarea.parser.AbstractParser;
import com.fr.design.gui.syntax.ui.rsyntaxtextarea.parser.DefaultParseResult;
import com.fr.design.gui.syntax.ui.rsyntaxtextarea.parser.ParseResult;
import com.fr.design.gui.syntax.ui.rsyntaxtextarea.parser.Parser;


/**
 * Manages code folding in an instance of RSyntaxTextArea.
 *
 * @author Robert Futrell
 * @version 1.0
 */
public class FoldManager {

	private RSyntaxTextArea textArea;
	private FoldParser parser;
	private List<Fold> folds;
	private boolean codeFoldingEnabled;
	private PropertyChangeSupport support;


	/**
	 * Property fired when folds have been updated.
	 */
	public static final String PROPERTY_FOLDS_UPDATED = "FoldsUpdated";


	/**
	 * Constructor.
	 *
	 * @param textArea The text area whose folds we are managing.
	 */
	public FoldManager(RSyntaxTextArea textArea) {
		this.textArea = textArea;
		support = new PropertyChangeSupport(this);
		Listener l = new Listener();
		textArea.getDocument().addDocumentListener(l);
		textArea.addPropertyChangeListener(RSyntaxTextArea.SYNTAX_STYLE_PROPERTY, l);
		folds = new ArrayList<Fold>();
		updateFoldParser(); 
	}


	/**
	 * Adds a property change listener to this fold manager.
	 *
	 * @param l The new listener.
	 * @see #removePropertyChangeListener(PropertyChangeListener)
	 */
	public void addPropertyChangeListener(PropertyChangeListener l) {
		support.addPropertyChangeListener(l);
	}


	/**
	 * Removes all folds.
	 */
	public void clear() {
		folds.clear();
	}


	/**
	 * Ensures that the specified offset is not hidden in a collapsed fold.
	 * Any folds containing this offset that are collapsed will be expanded.
	 *
	 * @param offs The offset.
	 * @return Whether any folds had to be opened.
	 * @see #getDeepestFoldContaining(int)
	 */
	public boolean ensureOffsetNotInClosedFold(int offs) {
		boolean foldsOpened = false;
		Fold fold = getDeepestFoldContaining(offs);
		while (fold!=null) {
			if (fold.isCollapsed()) {
				fold.setCollapsed(false);
				foldsOpened = true;
			}
			fold = fold.getParent();
		}
		return foldsOpened;
	}


	/**
	 * Returns the "deepest" nested fold containing the specified offset.
	 *
	 * @param offs The offset.
	 * @return The deepest fold containing the offset, or <code>null</code> if
	 *         no fold contains the offset.
	 */
	public Fold getDeepestFoldContaining(int offs) {
		Fold deepestFold = null;
		if (offs>-1) {
			for (int i=0; i<folds.size(); i++) {
				Fold fold = getFold(i);
				if (fold.containsOffset(offs)) {
					deepestFold = fold.getDeepestFoldContaining(offs);
					break;
				}
			}
		}
		return deepestFold;
	}


	/**
	 * Returns the "deepest" open fold containing the specified offset.
	 *
	 * @param offs The offset.
	 * @return The fold, or <code>null</code> if no open fold contains the
	 *         offset.
	 */
	public Fold getDeepestOpenFoldContaining(int offs) {

		Fold deepestFold = null;

		if (offs>-1) {
			for (int i=0; i<folds.size(); i++) {
				Fold fold = getFold(i);
				if (fold.containsOffset(offs)) {
					if (fold.isCollapsed()) {
						return null;
					}
					deepestFold = fold.getDeepestOpenFoldContaining(offs);
					break;
				}
			}
		}

		return deepestFold;

	}


	/**
	 * Returns a specific top-level fold, which may have child folds.
	 *
	 * @param index The index of the fold.
	 * @return The fold.
	 * @see #getFoldCount()
	 */
	public Fold getFold(int index) {
		return folds.get(index);
	}


	/**
	 * Returns the number of top-level folds.
	 *
	 * @return The number of top-level folds.
	 * @see #getFold(int)
	 */
	public int getFoldCount() {
		return folds.size();
	}


	/**
	 * Returns the fold region that starts at the specified line.
	 *
	 * @param line The line number.
	 * @return The fold, or <code>null</code> if the line is not the start
	 *         of a fold region.
	 * @see #isFoldStartLine(int)
	 */
	public Fold getFoldForLine(int line) {
		return getFoldForLineImpl(null, folds, line);
	}


private Fold getFoldForLineImpl(Fold parent, List<Fold> folds, int line) {

	int low = 0;
	int high = folds.size() - 1;

	while (low <= high) {
		int mid = (low + high) >> 1;
		Fold midFold = folds.get(mid);
		int startLine = midFold.getStartLine();
		if (line==startLine) {
			return midFold;
		}
		else if (line<startLine) {
			high = mid - 1;
		}
		else {
			int endLine = midFold.getEndLine();
			if (line>=endLine) {
				low = mid + 1;
			}
			else { // line>startLine && line<=endLine
				List<Fold> children = midFold.getChildren();
				return children!=null ? getFoldForLineImpl(midFold, children, line) : null;
			}
		}
	}

	return null; // No fold for this line
}


	/**
	 * Returns the total number of hidden (folded) lines.
	 *
	 * @return The total number of hidden (folded) lines.
	 * @see #getHiddenLineCountAbove(int)
	 */
	public int getHiddenLineCount() {
		int count = 0;
		for (Fold fold : folds) {
			count += fold.getCollapsedLineCount();
		}
		return count;
	}


	/**
	 * Returns the number of lines "hidden" by collapsed folds above the
	 * specified line.
	 *
	 * @param line The line.  This is the line number for a logical line.
	 *        For the line number of a physical line (i.e. visible, not folded),
	 *        use <code>getHiddenLineCountAbove(int, true)</code>.
	 * @return The number of lines hidden in folds above <code>line</code>.
	 * @see #getHiddenLineCountAbove(int, boolean)
	 */
	public int getHiddenLineCountAbove(int line) {
		return getHiddenLineCountAbove(line, false);
	}
	

	/**
	 * Returns the number of lines "hidden" by collapsed folds above the
	 * specified line.
	 *
	 * @param line The line.
	 * @param physical Whether <code>line</code> is the number of a physical
	 *        line (i.e. visible, not code-folded), or a logical one (i.e. any
	 *        line from the model).  If <code>line</code> was determined by a
	 *        raw line calculation (i.e. <code>(visibleTopY / lineHeight)</code>),
	 *        this value should be <code>true</code>.  It should be
	 *        <code>false</code> when it was calculated from an offset in the
	 *        document (for example).
	 * @return The number of lines hidden in folds above <code>line</code>.
	 */
	public int getHiddenLineCountAbove(int line, boolean physical) {

		int count = 0;

		for (Fold fold : folds) {
			int comp = physical ? line+count : line;
			if (fold.getStartLine()>=comp) {
				break;
			}
			count += getHiddenLineCountAboveImpl(fold, comp, physical);
		}

		return count;

	}


	/**
	 * Returns the number of lines "hidden" by collapsed folds above the
	 * specified line.
	 *
	 * @param fold The current fold in the recursive algorithm.  It and its
	 *        children are examined.
	 * @param line The line.
	 * @param physical Whether <code>line</code> is the number of a physical
	 *        line (i.e. visible, not code-folded), or a logical one (i.e. any
	 *        line from the model).  If <code>line</code> was determined by a
	 *        raw line calculation (i.e. <code>(visibleTopY / lineHeight)</code>),
	 *        this value should be <code>true</code>.  It should be
	 *        <code>false</code> when it was calculated from an offset in the
	 *        document (for example).
	 * @return The number of lines hidden in folds that are descendants of
	 *         <code>fold</code>, or <code>fold</code> itself, above
	 *         <code>line</code>.
	 */
	private int getHiddenLineCountAboveImpl(Fold fold, int line, boolean physical) {

		int count = 0;

		if (fold.getEndLine()<line ||
				(fold.isCollapsed() && fold.getStartLine()<line)) {
			count = fold.getCollapsedLineCount();
		}
		else {
			int childCount = fold.getChildCount();
			for (int i=0; i<childCount; i++) {
				Fold child = fold.getChild(i);
				int comp = physical ? line+count : line;
				if (child.getStartLine()>=comp) {
					break;
				}
				count += getHiddenLineCountAboveImpl(child, comp, physical);
			}
		}

		return count;

	}


	/**
	 * Returns the last visible line in the text area, taking into account
	 * folds.
	 *
	 * @return The last visible line.
	 */
	public int getLastVisibleLine() {

		int lastLine = textArea.getLineCount() - 1;

		if (isCodeFoldingSupportedAndEnabled()) {
			int foldCount = getFoldCount();
			if (foldCount>0) {
				Fold lastFold = getFold(foldCount-1);
				if (lastFold.containsLine(lastLine)) {
					if (lastFold.isCollapsed()) {
						lastLine = lastFold.getStartLine();
					}
					else { // Child fold may end on the same line as parent
						while (lastFold.getHasChildFolds()) {
							lastFold = lastFold.getLastChild();
							if (lastFold.containsLine(lastLine)) {
								if (lastFold.isCollapsed()) {
									lastLine = lastFold.getStartLine();
									break;
								}
							}
							else { // Higher up
								break;
							}
						}
					}
				}
			}
		}

		return lastLine;

	}


	public int getVisibleLineAbove(int line) {

		if (line<=0 || line>=textArea.getLineCount()) {
			return -1;
		}

		do {
			line--;
		} while (line>=0 && isLineHidden(line));

		return line;

	}


	public int getVisibleLineBelow(int line) {

		int lineCount = textArea.getLineCount();
		if (line<0 || line>=lineCount-1) {
			return -1;
		}

		do {
			line++;
		} while (line<lineCount && isLineHidden(line));

		return line==lineCount ? -1 : line;

	}


//	private static int binaryFindFoldContainingLine(int line) {
//
//List allFolds;
//
//		int low = 0;
//		int high = allFolds.size() - 1;
//
//		while (low <= high) {
//			int mid = (low + high) >> 1;
//			Fold midVal = (Fold)allFolds.get(mid);
//			if (midVal.containsLine(line)) {
//				return mid;
//			}
//			if (line<=midVal.getStartLine()) {
//				high = mid - 1;
//			}
//			else { // line > midVal.getEndLine()
//				low = mid + 1;
//			}
//		}
//
//		return -(low + 1); // key not found
//
//	}


    /**
	 * Returns whether code folding is enabled.  Note that only certain
	 * languages support code folding; those that do not will ignore this
	 * property.
	 *
	 * @return Whether code folding is enabled.
	 * @see #setCodeFoldingEnabled(boolean)
	 */
	public boolean isCodeFoldingEnabled() {
		return codeFoldingEnabled;
	}


	/**
	 * Returns <code>true</code> if and only if code folding is enabled for
	 * this text area, AND folding is supported for the language it is editing.
	 * Whether or not folding is supported for a language depends on whether
	 * a fold parser is registered for that language with the
	 * <code>FoldParserManager</code>.
	 *
	 * @return Whether folding is supported and enabled for this text area.
	 * @see FoldParserManager
	 */
	public boolean isCodeFoldingSupportedAndEnabled() {
		return codeFoldingEnabled && parser!=null;
	}


	/**
	 * Returns whether the specified line contains the start of a fold region.
	 *
	 * @param line The line.
	 * @return Whether the line contains the start of a fold region.
	 * @see #getFoldForLine(int)
	 */
	public boolean isFoldStartLine(int line) {
		return getFoldForLine(line)!=null;
	}


	/**
	 * Returns whether a line is hidden in a collapsed fold.
	 *
	 * @param line The line to check.
	 * @return Whether the line is hidden in a collapsed fold.
	 */
	public boolean isLineHidden(int line) {
		for (Fold fold : folds) {
			if (fold.containsLine(line)) {
				if (fold.isCollapsed()) {
					return true;
				}
				else {
					return isLineHiddenImpl(fold, line);
				}
			}
		}
		return false;
	}


	private boolean isLineHiddenImpl(Fold parent, int line) {
		for (int i=0; i<parent.getChildCount(); i++) {
			Fold child = parent.getChild(i);
			if (child.containsLine(line)) {
				if (child.isCollapsed()) {
					return true;
				}
				else {
					return isLineHiddenImpl(child, line);
				}
			}
		}
		return false;
	}


	/**
	 * Checks whether a single fold was there in the "old" set of folds.  If
	 * it was, its collapsed state is preserved.
	 *
	 * @param newFold The "new" fold to check for.
	 * @param oldFolds The previous folds before an edit occurred.
	 */
	private void keepFoldState(Fold newFold, List<Fold> oldFolds) {
		int previousLoc = Collections.binarySearch(oldFolds, newFold);
		//System.out.println(newFold + " => " + previousLoc);
		if (previousLoc>=0) {
			Fold prevFold = oldFolds.get(previousLoc);
			newFold.setCollapsed(prevFold.isCollapsed());
		}
		else {
			//previousLoc = -(insertion point) - 1;
			int insertionPoint = -(previousLoc + 1);
			if (insertionPoint>0) {
				Fold possibleParentFold = oldFolds.get(insertionPoint-1);
				if (possibleParentFold.containsOffset(
						newFold.getStartOffset())) {
					List<Fold> children = possibleParentFold.getChildren();
					if (children!=null) {
						keepFoldState(newFold, children);
					}
				}
			}
		}
	}


	/**
	 * Called when new folds come in from the fold parser.  Checks whether any
	 * folds from the "old" fold list are still in the "new" list; if so, their
	 * collapsed state is preserved.
	 *
	 * @param newFolds The "new" folds after an edit occurred.  This cannot be
	 *        <code>null</code>.
	 * @param oldFolds The previous folds before the edit occurred.
	 */
	private void keepFoldStates(List<Fold> newFolds, List<Fold> oldFolds) {
		for (Fold newFold : newFolds) {
			keepFoldState(newFold, folds);
			List<Fold> newChildFolds = newFold.getChildren();
			if (newChildFolds!=null) {
				keepFoldStates(newChildFolds, oldFolds);
			}
		}
	}


	/**
	 * Removes a property change listener from this fold manager.
	 *
	 * @param l The listener to remove.
	 * @see #addPropertyChangeListener(PropertyChangeListener)
	 */
	public void removePropertyChangeListener(PropertyChangeListener l) {
		support.removePropertyChangeListener(l);
	}


	/**
	 * Forces an immediate reparsing for folds, if folding is enabled.  This
	 * usually does not need to be called by the programmer, since fold
	 * parsing is done automatically by RSTA.
	 */
	public void reparse() {

		if (codeFoldingEnabled && parser!=null) {

			// Re-calculate folds.  Keep the fold state of folds that are
			// still around.
			List<Fold> newFolds = parser.getFolds(textArea);
			if (newFolds==null) {
				newFolds = Collections.emptyList();
			}
			else {
				keepFoldStates(newFolds, folds);
			}
			folds = newFolds;

			// Let folks (gutter, etc.) know that folds have been updated.
			support.firePropertyChange(PROPERTY_FOLDS_UPDATED, null, folds);
			textArea.repaint();

		}
		else {
			folds.clear();
		}

	}


	/**
	 * Sets whether code folding is enabled.  Note that only certain
	 * languages will support code folding out of the box.  Those languages
	 * which do not support folding will ignore this property.
	 *
	 * @param enabled Whether code folding should be enabled.
	 * @see #isCodeFoldingEnabled()
	 */
	public void setCodeFoldingEnabled(boolean enabled) {
		if (enabled!=codeFoldingEnabled) {
			codeFoldingEnabled = enabled;
			if (tempParser!=null) {
				textArea.removeParser(tempParser);
			}
			if (enabled) {
				tempParser = new AbstractParser() {
					public ParseResult parse(RSyntaxDocument doc, String style) {
						reparse();
						return new DefaultParseResult(this);
					}
				};
				textArea.addParser(tempParser);
				support.firePropertyChange(PROPERTY_FOLDS_UPDATED, null, null);
				//reparse();
			}
			else {
				folds = Collections.emptyList();
				textArea.repaint();
				support.firePropertyChange(PROPERTY_FOLDS_UPDATED, null, null);
			}
		}
	}
private Parser tempParser;


	/**
	 * Sets the folds for this fold manager.
	 *
	 * @param folds The new folds.  This should not be <code>null</code>.
	 */
	public void setFolds(List<Fold> folds) {
		this.folds = folds;
	}


	/**
	 * Updates the fold parser to be the one appropriate for the language
	 * currently being highlighted.
	 */
	private void updateFoldParser() {
		parser = FoldParserManager.get().getFoldParser(
											textArea.getSyntaxEditingStyle());
	}


	/**
	 * Listens for events in the text editor.
	 */
	private class Listener implements DocumentListener, PropertyChangeListener {

		public void changedUpdate(DocumentEvent e) {
		}

		public void insertUpdate(DocumentEvent e) {
			// Adding text containing a newline to the visible line of a folded
			// Fold causes that Fold to unfold.  Check only start offset of
			// insertion since that's the line that was "modified".
			int startOffs = e.getOffset();
			int endOffs = startOffs + e.getLength();
			Document doc = e.getDocument();
			Element root = doc.getDefaultRootElement();
			int startLine = root.getElementIndex(startOffs);
			int endLine = root.getElementIndex(endOffs);
			if (startLine!=endLine) { // Inserted text covering > 1 line...
				Fold fold = getFoldForLine(startLine);
				if (fold!=null && fold.isCollapsed()) {
					fold.toggleCollapsedState();
				}
			}
		}

		public void propertyChange(PropertyChangeEvent e) {
			// Syntax style changed in editor.
			updateFoldParser();
			reparse(); // Even if no fold parser change, highlighting did
		}
		public void removeUpdate(DocumentEvent e) {
			// Removing text from the visible line of a folded Fold causes that
			// Fold to unfold.  We only need to check the removal offset since
			// that's the new caret position.
			int offs = e.getOffset();
			try {
				int lastLineModified = textArea.getLineOfOffset(offs);
				//System.out.println(">>> " + lastLineModified);
				Fold fold = getFoldForLine(lastLineModified);
				//System.out.println("&&& " + fold);
				if (fold!=null && fold.isCollapsed()) {
					fold.toggleCollapsedState();
				}
			} catch (BadLocationException ble) {
				ble.printStackTrace(); // Never happens
			}
		}

	}


}