/*******************************************************************************
 * Copyright (c) 2000, 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.swt.custom;

import org.eclipse.swt.graphics.*;
import org.eclipse.swt.internal.Compatibility;
import java.util.Vector;

class DefaultLineStyler implements LineStyleListener, LineBackgroundListener {
	StyledTextContent content;
	StyleRange styles[] = new StyleRange[0];
	int styleCount = 0;	// the number of styles	
	int lineExpandExp = 1; 	// the expansion exponent, used to increase the lines array exponentially
	int lineCount = 0;
	Color lineBackgrounds[];
	
/** 
 * Creates a new default line styler.
 * <p>
 *
 * @param content the text to which the styles apply
 */
public DefaultLineStyler(StyledTextContent content) {
	this.content = content;
	lineCount = content.getLineCount();
	lineBackgrounds = new Color[lineCount];
}	
/** 
 * Inserts a style at the given location.
 * <p>
 *
 * @param style	the new style
 * @param index	the index at which to insert the style (the new style
 * 	will reside at this index)
 *
 */
void insertStyle(StyleRange style, int index) {
	insertStyles(new StyleRange[] {style}, index);
}
/** 
 * Insert the styles at the given location.
 * <p>
 *
 * @param insertStyles	the new styles
 * @param index	the index at which to insert the styles (the first new style
 * 	will reside at this index)
 *
 */
void insertStyles(StyleRange[] insertStyles, int index) {
	int size = styles.length;
	int insertCount = insertStyles.length;
	int spaceNeeded = styleCount + insertCount - size;
	if (spaceNeeded > 0) {
		StyleRange[] newStyles = new StyleRange[size+spaceNeeded];
		System.arraycopy(styles, 0, newStyles, 0, size);
		styles = newStyles;
	}
	// shift the styles down to make room for the new styles
	System.arraycopy(styles, index, styles, index + insertCount, styleCount - index);
	// add the new styles
	System.arraycopy(insertStyles, 0, styles, index, insertCount);
	styleCount = styleCount + insertCount;
}
/** 
 * Inserts a style, merging it with adjacent styles if possible.
 * <p>
 *
 * @param style	the new style
 * @param index	the index at which to insert the style (the new style
 * 	will reside at this index)
 * @return true if the style was inserted, false if the style was merged with an adjacent 
 * 	style
 */
boolean insertMergeStyle(StyleRange style, int index) {
	if (mergeStyleBefore(style, index)) return false;
	if (mergeStyleAfter(style, index)) return false;
	insertStyle(style, index);
	return true;
}
/** 
 * Merges the style with the style before it if possible.
 * <p>
 *
 * @param style	the new style
 * @param index	the index at which to attempt the merge.
 * @return true if the style was merged, false otherwise
 */
boolean mergeStyleBefore(StyleRange style, int index) {
	// see if the style is similar to the style before it and merge the
	// styles if possible
	if (index > 0) {
		StyleRange previous = styles[index-1];
		if (style.similarTo(previous)) {
			// the start of style needs to be in the range of the previous style
			// and the end of style needs to be < the start of the next style
			int previousEnd = previous.start + previous.length;
			if ((style.start <= previousEnd) && (style.start >= previous.start)) {
				int styleEnd = style.start + style.length;
				if ((index == styleCount) || (styleEnd <= styles[index].start)) {
					previous.length = style.start + style.length - previous.start;
					return true;
				}
			}
		}
	}
	return false;
}
/** 
 * Merges the style with the style after it if possible.
 * <p>
 *
 * @param style	the new style
 * @param index	the index at which to attempt the merge.
 * @return true if the style was merged, false otherwise
 */
boolean mergeStyleAfter(StyleRange style, int index) {
	// see if the style is similar to the style that will be after it and 
	// merge the styles if possible
	if (index < styleCount) {
		StyleRange next = styles[index];
		if (style.similarTo(next)) {
			// the end of style needs to be in the range of the next style and
			// the start of style needs to be > the end of the previous style
			int styleEnd = style.start + style.length;
			int nextEnd = next.start + next.length;
			if ((styleEnd <= nextEnd) && (styleEnd >= next.start)) {
				if ((index == 0) || (style.start >= styles[index-1].start + styles[index-1].length)) {
					next.length = next.start + next.length - style.start;
					next.start = style.start;
					return true;
				}
			}
		}
	}
	return false;
}
/** 
 * Removes style information that is defined for the range of text in <code>clearStyle</code>.
 * <p>
 *
 * @param clearStyle the style information to use for clearing 
 */ 
void clearStyle(StyleRange clearStyle) {
	Point pt = getOverlappingStyles(clearStyle.start, clearStyle.length);
	int clearStyleEnd = clearStyle.start + clearStyle.length - 1;
	
	// no overlapped styles exist
	if ((pt == null) || (pt.y == 0)) return; 

	// the newStyle overlaps one or more of the existing styles
	// pt.x is the index of the first overlapped style, pt.y is the number of overlapped
	// styles
	int count = 0;
	int deleteStyle = -1;
	int deleteCount = 0;
	for (int i=pt.x; count<pt.y; i++) {
		StyleRange overlap = styles[i];
		int overlapEnd = overlap.start + overlap.length - 1;
		if (overlap.start < clearStyle.start) {
			if (overlapEnd <= clearStyleEnd) {
				// the end of overlap needs to be cleared
				overlap.length=clearStyle.start - overlap.start;
			} else {
				// middle of overlap needs to be cleared, this will
				// cause overlap to be broken into two
				StyleRange endStyle = (StyleRange)overlap.clone();
				endStyle.start = clearStyleEnd + 1;
				endStyle.length = overlapEnd - clearStyleEnd;
				overlap.length = clearStyle.start - overlap.start;
				insertStyle(endStyle, i+1);
				break;
			}
		} else {
			if (overlapEnd <= clearStyleEnd) {	
				// entire overlap needs to be cleared
				if (deleteStyle == -1) {
					deleteStyle = i;
				} 
				deleteCount++;
			} else {
				// beginning of overlap needs to be cleared
				overlap.start=clearStyleEnd + 1;
				overlap.length=overlapEnd - overlap.start + 1;
				break;
			}
		}				
		count++;
	}
	deleteStyles(deleteStyle, deleteCount);
}
/**
 * Increases the <code>linebackgrounds</code> array to accomodate new line background
 * information.
 * <p>
 *
 * @param numLines the number to increase the array by
 */
void expandLinesBy(int numLines) {
	int size = lineBackgrounds.length;
	if (size - lineCount >= numLines) {
		return;
	}
	Color[] newLines = new Color[size+Math.max(Compatibility.pow2(lineExpandExp), numLines)];
	System.arraycopy(lineBackgrounds, 0, newLines, 0, size);
	lineBackgrounds = newLines;
	lineExpandExp++;
}
/** 
 * Deletes the style at <code>index</code>.
 * <p>
 *
 * @param index	the index of the style to be deleted
 */
void deleteStyle(int index) {
	deleteStyles(index, 1);
}
/** 
 * Delete count styles starting at <code>index</code>.
 * <p>
 *
 * @param index	the index of the style to be deleted
 * @param count	the number of styles to be deleted
 */
void deleteStyles(int index, int count) {
	if ((count == 0) || (index < 0)) return;
	// shift the styles up 
	System.arraycopy(styles, index + count, styles, index, styleCount - (index + count));
	for (int i=0; i<count; i++) {
		styles[styleCount - i - 1] = null;
	}
	styleCount = styleCount - count;
}
/** 
 * Returns the styles that are defined.
 * <p>
 *
 * @return the copied array of styles
 */
StyleRange [] getStyleRanges() {
	StyleRange[] newStyles = new StyleRange[styleCount];
	System.arraycopy(styles, 0, newStyles, 0, styleCount);
	return newStyles;
}
/**
 * Handles the get line background color callback.
 * <p>
 *
 * @param event the lineOffset line number (input), lineText line text (input),
 * and background line background color (output)
 */
public void lineGetBackground(LineBackgroundEvent event) {
	int lineIndex = content.getLineAtOffset(event.lineOffset);
	event.lineBackground = lineBackgrounds[lineIndex];
}
/**
 * Handles the get line style information callback.
 * <p>
 *
 * @param event the lineOffset line number (input), lineText line text (input),
 * and styles array of StyleRanges, need to be in order (output)
 */
public void lineGetStyle(LineStyleEvent event) {
	int lineStart = event.lineOffset;
	int lineEnd = lineStart + event.lineText.length();

	int high = searchForStyle(lineStart, lineEnd);
	StyleRange style = null;
	Vector lineStyles = new Vector();

	// index will represent a style that 
	// -- starts after the line (end processing)
	// -- ends before the line (continue processing)
	// -- starts before the line, ends in the line (add range)
	// -- starts in the line, ends in the line (add range)
	// -- starts in the line, ends after the line (add range)
	// -- starts before the line, ends after the line (add range)
	for (int index = high; index < styleCount; index++) {
		style = styles[index];
		if (style.start > lineEnd)
			// style starts after the line, end looping 
			break;
		int styleEnd = style.start + style.length - 1;
		if (styleEnd >= lineStart) lineStyles.addElement(style);
	}
	event.styles = new StyleRange[lineStyles.size()];
	lineStyles.copyInto(event.styles);
}
/** 
 * Searches for the first style in the <code>start</code> - <code>end</code> range.
 * <p>
 *
 * @return the index of the first style that overlaps the input range  
 */
int searchForStyle(int start, int end) {
	int high = styleCount;
	int low = -1;
	int index = high;
	// find the index of the first style for the given range, use a binary search
	while (high - low > 1) {
		index = (high + low) / 2;
		StyleRange style = styles[index];
		int styleEnd = style.start + style.length - 1;
		if (start <= style.start || end <= styleEnd || (start > style.start && styleEnd >= start && styleEnd < end)) {
			high = index;			
		}
		else {
			low = index;
		}
	}
	return high;
}
/** 
 * Updates the line background colors to reflect a new color.  Called by StyledText.
 * <p>
 *
 * @param startLine index of the first line to color
 * @param count number of lines to color starting at startLine
 * @param background the background color for the lines
 */ 
void setLineBackground(int startLine, int count, Color background) {
	for (int i=startLine; i<startLine + count; i++) {
		lineBackgrounds[i]=background;
	}
}
/** 
 * Update the styles to reflect the new style.  <code>newStyle</code> will 
 * replace any old style for the range.  When this method is called, the 
 * DefaultLineStyler may merge the new style with an existing style (if possible).
 * Called by StyledText when a style is added.  Called by StyledText.
 * <p>
 *
 * @param newStyle the new style information.
 */ 
void setStyleRange(StyleRange newStyle) {
	if (newStyle == null) {
		styles = new StyleRange[0];
		styleCount = 0;
		return;
	}
	if (newStyle.length ==0) return;
	if (newStyle.isUnstyled()) {
		clearStyle(newStyle);
		return;
	}
	
	Point pt = getOverlappingStyles(newStyle.start, newStyle.length);
	int newStyleEnd = newStyle.start + newStyle.length - 1;
	
	// no styles exist
	if (pt == null) {
		insertStyle(newStyle, 0);
		return;
	}
	
	// newStyle does not overlap any other styles
	if (pt.y == 0) {
		insertMergeStyle(newStyle, pt.x);
		return;
	} 

	// the newStyle overlaps one or more of the existing styles
	boolean added = false; // indicates whether or not the new style has been added
	int count = 0;
	// pt.x is the index of the first overlapped style, pt.y is the number of overlapped
	// styles
	for (int i=pt.x; count<pt.y; i++) {
		StyleRange overlap = styles[i];
		int overlapEnd = overlap.start + overlap.length - 1;
		if (overlap.start < newStyle.start) {
			if (overlapEnd <= newStyleEnd) {
				// the end of overlap needs to be replaced by newStyle
				if (newStyle.similarTo(overlap)) {
					// update overlap to accomodate the new style
					overlap.length = newStyle.start + newStyle.length - overlap.start;
				} else {
					overlap.length=newStyle.start - overlap.start;
					// see if newStyle can be merged with the style after overlap, if so,
					// processing is done
					if (mergeStyleAfter(newStyle, i+1)) break;
					// otherwise, insert the newStyle, newStyle may still overlap other
					// styles after it so continue processing	
					insertStyle(newStyle, i+1);
					i++;
				}
				added = true;
			} else {
				// middle of overlap needs to be replaced by newStyle, this will
				// cause overlap to be broken into two
				if (newStyle.similarTo(overlap)) break;
				StyleRange endStyle = (StyleRange)overlap.clone();
				endStyle.start = newStyleEnd + 1;
				endStyle.length = overlapEnd - newStyleEnd;
				overlap.length = newStyle.start - overlap.start;
				insertStyle(newStyle, i+1);
				i++;
				insertStyle(endStyle, i+1);
				// when newStyle overlaps the middle of a style, this implies that
				// processing is done (no more overlapped styles)
				break;
			}
		} else {
			if (overlapEnd <= newStyleEnd) {	
				// overlap will be replaced by the newStyle, make sure newStyle
				// hasn't already been added, if it has just delete overlap
				if (!added) {
					styles[i] = newStyle;
					added = true;
				} else {
					deleteStyle(i);
					i--;
				}
			} else {
				// beginning of overlap needs to be replaced by newStyle
				overlap.start=newStyleEnd + 1;
				overlap.length=overlapEnd - overlap.start + 1;
				if (!added) {
					insertMergeStyle(newStyle, i);
				}
				// when newStyle overlaps only the beginning of a style, this implies 
				// that processing is done (no more overlapped styles)
				break;
			}
		}				
	count++;
	}
}
/** 
 * Replace the styles for the given range.
 *
 * @param start the initial style range to replace
 * @param length the number of ranges to replace
 * @param ranges the new styles, must be in order and non-overlapping
 */
void replaceStyleRanges(int start, int length, StyleRange[] ranges) {
	clearStyle(new StyleRange(start, length, null, null));
	// find insert point
	int high = styleCount;
	int low = -1;
	int index = high;
	while (high - low > 1) {
		index = (high + low) / 2;
		StyleRange style = styles[index];
		if (start <= style.start) {
			high = index;			
		}
		else {
			low = index;
		}
	}
	insertStyles(ranges, high);
}
/** 
 * Sets the array of styles and discards old styles.  Called by StyledText.
 * <p>
 *
 * @param styles the new styles, must be in order and non-overlapping
 */
void setStyleRanges(StyleRange[] styles) {
	this.styles = new StyleRange[styles.length];
	System.arraycopy(styles, 0, this.styles, 0, styles.length);
	styleCount = styles.length;
}
/** 
 * Updates the style ranges and line backgrounds to reflect a pending text 
 * change.
 * Called by StyledText when a TextChangingEvent is received.
 * <p>
 *
 * @param event	the event with the text change information
 */  
public void textChanging(TextChangingEvent event) {
	int startLine = content.getLineAtOffset(event.start);
	int startLineOffset = content.getOffsetAtLine(startLine);
	
	textChanging(event.start, -event.replaceCharCount);
	textChanging(event.start, event.newCharCount);
	
	if (event.replaceCharCount == content.getCharCount()) {
		// all text is going to be replaced, clear line backgrounds
		linesChanging(0, -lineCount);
		linesChanging(0, content.getLineCount() - event.replaceLineCount + event.newLineCount);
		return;
	}

	if (event.start != startLineOffset) {
		startLine = startLine + 1;
	}
			
	linesChanging(startLine, -event.replaceLineCount);
	linesChanging(startLine, event.newLineCount);
}
/*
 * Updates the line backgrounds to reflect a pending text change.
 * <p>
 *
 * @param start	the starting line of the change that is about to take place
 * @param delta	the number of lines in the change, > 0 indicates lines inserted,
 * 	< 0 indicates lines deleted
 */
void linesChanging(int start, int delta) {
	if (delta == 0) return;
	boolean inserting = delta > 0;
	if (inserting) {
		// shift the lines down to make room for new lines
		expandLinesBy(delta);
		for (int i = lineCount-1; i >= start; i--) {
			lineBackgrounds[i + delta]=lineBackgrounds[i];
		}
		for (int i=start; i<start + delta; i++) {
			lineBackgrounds[i]=null;
		}
	} else {
		// shift up the lines
		for (int i = start - delta; i < lineCount; i++) {
			lineBackgrounds[i+delta]=lineBackgrounds[i];
		}
	}
	lineCount += delta;
}
/*
 * Updates the style ranges to reflect a text change.
 * <p>
 *
 * @param start	the starting offset of the change that is about to 
 *	take place
 * @param delta	the length of the change, > 0 indicates text inserted,
 * 	< 0 indicates text deleted
 */
void textChanging(int start, int delta) {
	if (delta == 0) return;
	StyleRange style;
	// find the index of the first style for the given offset, use a binary search
	// to find the index
	int end;
	int deleteStart = -1;
	int deleteCount = 0;
	boolean inserting = delta > 0;
	if (inserting) {
		end = (start + delta) - 1;
	} else {
		end = (start - delta) - 1;
	}
	int high = searchForStyle(start, end);
	int index;
	// update the styles that are in the affected range
	for (index = high; index < styleCount; index++) {
		style = styles[index];
		if (inserting) {
			if (style.start >= start) break;
			// in the insert case only one style range will be directly affected,
			// it will need to be split into two and then the newStyle inserted
			StyleRange beforeStyle = (StyleRange)style.clone();
			beforeStyle.length = start - style.start;
			style.start = start;
			style.length = style.length - beforeStyle.length;
			if (beforeStyle.length != 0) insertStyle(beforeStyle, index);
			index++;
			break;
		} else {
			int styleEnd = style.start + style.length - 1;
			if (style.start > end) break;
			// in the delete case, any style that overlaps the change range will be
			// affected
			if (style.start < start) {
				if (styleEnd <= end) {
					// style starts before change range, ends in change range
					style.length = start - style.start;
				} else {
					// style starts before change range, ends after change range
					style.length = style.length + delta;
					index++;
					break;
				}
			} else {
				if (styleEnd <= end) {
					// style starts in change range, ends in change range
					if (deleteStart == -1)	{
						deleteStart = index;
					} 
					deleteCount++;
				} else {
					// style starts in change range, ends after change range
					style.start = start;
					style.length = styleEnd - end;
					index++;
					break;
				}
			}	
		}
	}
	deleteStyles(deleteStart, deleteCount);
	// change the offsets of the styles after the affected styles
	for (int i = index - deleteCount; i < styleCount; i++) {
		style = styles[i];
		style.start = style.start + delta;
	}
}
/** 
 * Returns the indexes of the styles that overlap the given range.  Styles that partially
 * or fully overlap the range will be returned.
 * <p>
 *
 * @return Point where x is the index of the starting overlap style, y is the number of
 * 	styles that overlap the range
 */ 
Point getOverlappingStyles(int start, int length) {	
	StyleRange style;
	if (styleCount == 0) return null;
	// find the index of the first style for the given offset, use a binary search
	// to find the index
	int end = start + length - 1;
	int high = searchForStyle(start, end);
	int count = 0;
	for (int index = high; index < styleCount; index++) {
		style = styles[index];
		int styleEnd = style.start + style.length - 1;
		if (style.start > end) break;
		if (styleEnd >= start) count++;	
	}
	return new Point(high, count);
}
/** 
 * Returns the background color of a line.  Called by StyledText.  It is safe to return 
 * the existing Color object since the colors are set and managed by the client.
 * <p>
 *
 * @param index	the line index
 * @return the background color of the line at the given index
 */ 
Color getLineBackground(int index) {
	return lineBackgrounds[index];
}
/** 
 * Returns the style for the character at <code>offset</code>.  Called by StyledText.  
 * Returns a new style.  Does not return the existing style.
 * <p>
 *
 * @param offset the character position in the text
 * @return a cloned StyleRange with start == offset and length == 1 if a style is
 * 	specified or null if no style is specified
 */ 
StyleRange getStyleRangeAtOffset(int offset) {
	if (styleCount == 0) return null;
	Point pt = getOverlappingStyles(offset, 1);
	if (pt == null || pt.y == 0) return null;
	StyleRange newStyle = (StyleRange)styles[pt.x].clone();
	newStyle.start = offset;
	newStyle.length = 1;
	return newStyle;
}
/** 
 * Returns the styles for the given range. Returns the existing styles,
 * so be careful not to modify the return value.  Styles are not cloned
 * in order to make this method as efficient as possible. 
 * <p>
 *
 * @param offset the start position of the text range
 * @param length the length of the text range
 * @return a StyleRange array or null if no styles are specified for the text
 * 	range
 */ 
StyleRange[] getStyleRangesFor(int offset, int length) {
	if (styleCount == 0) return null;
	Point pt = getOverlappingStyles(offset, length);
	if (pt == null || pt.y == 0) return null;
	StyleRange[] ranges = new StyleRange[pt.y];
	for (int i=0; i<pt.y; i++) {
		StyleRange newStyle = styles[pt.x + i];
		ranges[i]=newStyle;
	}
	return ranges;
}
void release () {
	styles = null;
}
}
