////////////////////////////////////////////////////////////////////////////////
//
// ADOBE SYSTEMS INCORPORATED
// Copyright 2007-2010 Adobe Systems Incorporated
// All Rights Reserved.
//
// NOTICE:  Adobe permits you to use, modify, and distribute this file 
// in accordance with the terms of the license agreement accompanying it.
//
////////////////////////////////////////////////////////////////////////////////
package flashx.textLayout.compose
{	
	import flash.text.engine.TextBlock;
	import flash.text.engine.TextLine;
	import flash.text.engine.TextLineValidity;
	
	import flashx.textLayout.container.ContainerController;
	import flashx.textLayout.debug.Debugging;
	import flashx.textLayout.debug.assert;
	import flashx.textLayout.elements.BackgroundManager;
	import flashx.textLayout.elements.FlowLeafElement;
	import flashx.textLayout.elements.TextFlow;
	import flashx.textLayout.tlf_internal;
	
	use namespace tlf_internal;
	
	[Exclude(name="initializeLines",kind="method")]
	[Exclude(name="addLine",kind="method")]
	[Exclude(name="lines",kind="property")]
	[Exclude(name="debugCheckTextFlowLines",kind="method")]
	[Exclude(name="checkFirstDamage",kind="method")]
	
	/** 
	 * The FlowComposerBase class is the base class for Text Layout Framework flow composer classes, which control the 
	 * composition of text lines in ContainerController objects.
	 *
	 * <p>FlowComposerBase is a utility class that implements methods and properties that are common
	 * to several types of flow composer. Application code would not typically instantiate or use this class
	 * (unless extending it to create a custom flow composer).</p>
	 * 
	 * @see flashx.textLayout.elements.TextFlow#flowComposer
	 * @playerversion Flash 10
	 * @playerversion AIR 1.5
	 * @langversion 3.0
	 */
	 
	public class FlowComposerBase
	{
				// Composition data
		[ ArrayElementType("text.elements.TextFlowLine") ]
		private var _lines:Array;	
		
		/** @private */
		protected var _textFlow:TextFlow;
		
		/** Absolute start of the damage area -- first character in the flow that is dirty and needs to be recomposed. @private */
		protected var _damageAbsoluteStart:int;
		
		/** @private */
		protected var _swfContext:ISWFContext;
		
		/** Constructor. 
		 *
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
	 	 * @langversion 3.0
	 	 */
	 	 
		public function FlowComposerBase()
		{
			_lines = new Array();
			_swfContext = null;
		}
		
		/** Returns the array of lines. @private */
		public function get lines():Array
		{ return _lines; }
		
		/** 
		 * @copy IFlowComposer#getLineAt()
		 * 
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
	 	 * @langversion 3.0
		 */
		 
		public function getLineAt(index:int):TextFlowLine
		{ return _lines[index]; }
		
		/** @copy IFlowComposer#numLines 
		 *
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
	 	 * @langversion 3.0
	 	 */
	 	 
		public function get numLines():int
		{ return _lines.length; }
		
		/** 
		* The TextFlow object to which this flow composer is attached. 
		*
		* @see flashx.textLayout.elements.TextFlow
		*
		* @playerversion Flash 10
		* @playerversion AIR 1.5
	 	* @langversion 3.0
	 	*/
	 	
		public function get textFlow():TextFlow
		{ return _textFlow; }
		
		/**
		 * The absolute position immediately preceding the first element in the text
		 * flow that requires composition and updating.
		 * 
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
	 	 * @langversion 3.0
		 */
		public function get damageAbsoluteStart():int
		{
			return _damageAbsoluteStart;
		}
		
		/**
		 * Initialize the lines for the TextFlow.  Creates a single TextFlowLine with no content. @private
		 */ 
		protected function initializeLines():void
		{
			var backgroundManager:BackgroundManager = _textFlow ? _textFlow.backgroundManager : null;
			// remove all the lines we have now - cache for reuse
			if (TextLineRecycler.textLineRecyclerEnabled)
			{
				for each (var line:TextFlowLine in _lines)
				{
					var textLine:TextLine = line.peekTextLine();
					if (textLine && !textLine.parent)
					{
						// releasing all textLines so release each still connected textBlock
						if (textLine.validity != TextLineValidity.INVALID)
						{
							var textBlock:TextBlock = textLine.textBlock;
							CONFIG::debug { Debugging.traceFTECall(null,textBlock,"releaseLines",textBlock.firstLine,textBlock.lastLine); }
							textBlock.releaseLines(textBlock.firstLine,textBlock.lastLine);
						}
						textLine.userData = null;
						TextLineRecycler.addLineForReuse(textLine);
						if (backgroundManager)
							backgroundManager.removeLineFromCache(textLine);
					}
				}
			}
			_lines.splice(0);
			_damageAbsoluteStart = 0;
			
			CONFIG::debug { checkFirstDamaged(); }
		}
		
		/** Make sure that there is a TextFlowLine for all the content - even if compose has stopped early. @private */
		protected function finalizeLinesAfterCompose():void
		{
			var line:TextFlowLine;
			if (_lines.length == 0)
			{
				// create a new line, with damage, that covers the entire area
				line = new TextFlowLine(null,null);
				line.setTextLength(textFlow.textLength);
				_lines.push(line);
			}
			else
			{
				line = _lines[_lines.length-1];
				var lineEnd:int = line.absoluteStart + line.textLength;
				if (lineEnd < textFlow.textLength)
				{
					line = new TextFlowLine(null,null);
					line.setAbsoluteStart(lineEnd);
					line.setTextLength(textFlow.textLength-lineEnd);
					_lines.push(line);
				}
			}			
		}
		
		/** 
		 * @copy IFlowComposer#updateLengths()
		 * 
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
	 	 * @langversion 3.0
		 */
		public function updateLengths(startPosition:int,deltaLength:int):void
		{
			// no lines yet - skip it
			if (numLines == 0)
				return;
				
			var line:TextFlowLine;	// sratch line variable
			var lineIdx:int = findLineIndexAtPosition(startPosition);
				
			var damageStart:int = int.MAX_VALUE;			
			if (deltaLength > 0)
			{
				if (lineIdx == _lines.length)
				{
					line = _lines[_lines.length-1];
					CONFIG::debug { assert(line.absoluteStart+line.textLength == startPosition,"updateLengths bad startIdx"); }
					line.setTextLength(line.textLength + deltaLength);
				}
				else
				{
					line = _lines[lineIdx++];
					line.setTextLength(line.textLength + deltaLength);
				}
				damageStart = line.absoluteStart;
			}
			else
			{
				var lenToDel:int = -deltaLength;
				var curPos:int = 0;
				
				while (true)
				{
					line = _lines[lineIdx];
					line.setAbsoluteStart(line.absoluteStart + lenToDel + deltaLength);
					curPos = (startPosition > line.absoluteStart ? startPosition : line.absoluteStart);
					
					var lineEndIdx:int = line.absoluteStart + line.textLength;
					var deleteChars:int = 0;
					
					if (curPos + lenToDel <= lineEndIdx)		
					{
						if (curPos == line.absoluteStart)
							deleteChars = lenToDel;				//delete from begin of line to end of selection
						else if (curPos == startPosition)
							deleteChars = lenToDel;				//delete is all included in one line
						else
						{
							CONFIG::debug { assert(false, "insertText: should never happen");  }
						}								
					}
					else //(curPos + lenToDel > lineEndIdx)		//multiline delete
					{
						if (curPos == line.absoluteStart)
							deleteChars = line.textLength; 		//delete the whole line
						else
							deleteChars = lineEndIdx-curPos;	//delete from middle of line to end of line
					}		
						
					if (curPos == line.absoluteStart && curPos + deleteChars == lineEndIdx)		//the whole line is selected
					{
						lenToDel -= deleteChars;
						_lines.splice(lineIdx,1);			//lineIdx now points to the next line
					}
					else 									//partial line
					{
						if (damageStart > line.absoluteStart)
							damageStart = line.absoluteStart;
						line.setTextLength(line.textLength - deleteChars);
						lenToDel -= deleteChars;
						lineIdx++;
					}
					CONFIG::debug { assert(lenToDel >= 0,"updateLengths deleted too much"); }
					if (lenToDel <= 0)
							break;						
				}
			}
			
			for ( ; lineIdx < _lines.length; lineIdx++)
			{
				line = _lines[lineIdx];
				if (deltaLength >= 0)
					line.setAbsoluteStart(line.absoluteStart  + deltaLength);
				else
					line.setAbsoluteStart(line.absoluteStart > -deltaLength ? line.absoluteStart+deltaLength : 0);
			}
			
			if (_damageAbsoluteStart > damageStart)
				_damageAbsoluteStart = damageStart;
		}
		
		/** 
		 * @copy IFlowComposer#damage()
		 * 
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
	 	 * @langversion 3.0
		 */
		public function damage(startPosition:int, damageLength:int, damageType:String):void
		{
			// find the line at damageStart
			if (_lines.length == 0 || textFlow.textLength == 0)
				return;
				
			// This case the damageStart is at the end of the text.  This can happen if the last paragraph is deleted
			if (startPosition == textFlow.textLength)
				return;
				
			CONFIG::debug { assert(startPosition + damageLength <= textFlow.textLength, "Damaging past end of flow!"); }
			
			// Start damaging one line before the startPosition location in case some of the first "damaged" line will fit on the previous line.
			var lineIndex:int = findLineIndexAtPosition(startPosition);
			var leaf:FlowLeafElement = textFlow.findLeaf(startPosition);
			if (leaf && lineIndex > 0)
				lineIndex--;

			if (lines[lineIndex].absoluteStart < _damageAbsoluteStart)
				_damageAbsoluteStart = _lines[lineIndex].absoluteStart;
				
			CONFIG::debug { assert(lineIndex < _lines.length && _lines[lineIndex].absoluteStart <= startPosition + damageLength, "Missing line"); }
			
			while (lineIndex < _lines.length)
			{
				var line:TextFlowLine = _lines[lineIndex];
				
				// Changed to >= from >, as > seemed to damage too
				// many lines when editing tables. 
				// Should verify the correctness of this.
				if (line.absoluteStart >= startPosition+damageLength)
					break;
				
				line.damage(damageType);
				lineIndex++;
			}
		}
		
		/**
		 * @copy IFlowComposer#isDamaged()
		 * 
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
	 	 * @langversion 3.0
		 */
		 
		public function isDamaged(absolutePosition:int):Boolean
		{
			// Returns true if any text from _damageAbsoluteStart through absolutePosition needs to be recomposed
			
			// no lines - damaged
			if (_lines.length == 0)
				return true;
				
			CONFIG::debug { checkFirstDamaged(); }

			return _damageAbsoluteStart <= absolutePosition && _damageAbsoluteStart != textFlow.textLength;
		}

		/** @private */
		CONFIG::debug public function checkFirstDamaged():void
		{
			// find the line at start
			if (_lines.length == 0)
				return;
				
			var lineIndex:int = findLineIndexAtPosition(0);
			while (lineIndex < _lines.length)
			{
				if (_lines[lineIndex].isDamaged())
				{
				//	trace("is damaged");
					CONFIG::debug { assert(_lines[lineIndex].absoluteStart >= _damageAbsoluteStart, "_damageAbsoluteStart doesn't match actual line value"); } 
					return;
				}
				++lineIndex;				
			}
		//	trace("not damaged");
			return;
		}
		/** 
		 * @copy IFlowComposer#findLineIndexAtPosition()
		 * 
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
	 	 * @langversion 3.0
	 	 */
	 	 
		public function findLineIndexAtPosition(absolutePosition:int,preferPrevious:Boolean = false):int
		{	
			var lo:int = 0;
			var hi:int = _lines.length-1;
			while (lo <= hi)
			{
				var mid:int = (lo+hi)/2;
				var line:TextFlowLine = _lines[mid];
				if (line.absoluteStart <= absolutePosition)
				{
					if (preferPrevious)
					{
						if (line.absoluteStart + line.textLength >= absolutePosition)
							return mid;
					}
					else
					{
						if (line.absoluteStart + line.textLength > absolutePosition)
							return mid;
					}
					lo = mid+1;
				}
				else
					hi = mid-1;
			}
			return _lines.length;
		}
		
		/**
		 * @copy IFlowComposer#findLineAtPosition()
		 * 
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
	 	 * @langversion 3.0
		 */
		 
		public function findLineAtPosition(absolutePosition:int,preferPrevious:Boolean = false):TextFlowLine
		{
			return _lines[findLineIndexAtPosition(absolutePosition,preferPrevious)];
		}
		
		/**
		 * add a new line 
		 * Add a new line to the list of composed lines for the frame. Lines are sorted
		 * by the start location, and each line has a span. The start of the next line
		 * has to match the start of the previous line + the span of the previous line.
		 * The last line needs to end at the end of the text. Therefore, when we add a
		 * new line, we may need to adjust the span and/or start locations of other lines 
		 * in the text.
		 * @private
		 */
		public function addLine(newLine:TextFlowLine,workIndex:int):void
		{
			CONFIG::debug { assert(workIndex == findLineIndexAtPosition(newLine.absoluteStart),"bad workIndex to TextFlow.addLine"); }
			CONFIG::debug { assert (!newLine.isDamaged(), "adding damaged line"); }
			var workLine:TextFlowLine = _lines[workIndex];
			var afterLine:TextFlowLine;
			var damageStart:int = int.MAX_VALUE;
			if (_damageAbsoluteStart == newLine.absoluteStart)
				_damageAbsoluteStart = newLine.absoluteStart + newLine.textLength;
				
			if (workLine == null)
				lines.push(newLine);				
			else if (workLine.absoluteStart != newLine.absoluteStart)
			{
				if (workLine.absoluteStart + workLine.textLength > newLine.absoluteStart + newLine.textLength)
				{
					// Making a new line in the middle of an old one. Need to split the old one.
					afterLine = new TextFlowLine(null,newLine.paragraph);
					afterLine.setAbsoluteStart(newLine.absoluteStart + newLine.textLength);
					var oldCharCount:int = workLine.textLength;
					workLine.setTextLength(newLine.absoluteStart - workLine.absoluteStart);
					CONFIG::debug { assert(workLine.textLength != 0, "0 width line"); }
					afterLine.setTextLength((oldCharCount - newLine.textLength) - workLine.textLength);
					CONFIG::debug { assert(afterLine.textLength != 0, "0 width line"); }
					_lines.splice(workIndex + 1, 0, newLine, afterLine);
				}
				else
				{
					// We're composing ahead, so we need to split the line where we're at
					// This can happen if a table is getting composed, some cells can be composed before 
					// others that go before. 
					CONFIG::debug { assert(workLine.isDamaged(), "Uneven line boundary, but lines marked up to date"); }
					workLine.setTextLength(newLine.absoluteStart - workLine.absoluteStart);
					CONFIG::debug { assert(workLine.textLength != 0, "0 width line"); }
					afterLine = _lines[workIndex+1];
					afterLine.setTextLength((newLine.absoluteStart + newLine.textLength) - afterLine.absoluteStart);
					CONFIG::debug { assert(_lines[workIndex + 1].textLength != 0, "0 width line"); }
					afterLine.setAbsoluteStart(newLine.absoluteStart + newLine.textLength);
					_lines.splice(workIndex + 1, 0, newLine);
				}
				damageStart = workLine.absoluteStart;
			}
			else if (workLine.textLength > newLine.textLength)
			{
				// New line partially overlaps old line.
				// Keep the old line, but resize it so it comes after the new line.
				// Insert the new line at the old line's position
				workLine.setTextLength(workLine.textLength - newLine.textLength);
				CONFIG::debug { assert(workLine.textLength != 0, "0 width line"); }
				workLine.setAbsoluteStart(newLine.absoluteStart + newLine.textLength);
				workLine.damage(TextLineValidity.INVALID);
				_lines.splice(workIndex, 0, newLine);
				damageStart = workLine.absoluteStart;
			}
			else 
			{
				var deleteCount:int = 1;
				// The new line completely overlaps the old line.
				// Insert the new line over the old line. If the line extents don't match,
				// fix-up the starting position & extent of the following line.
				if (workLine.textLength != newLine.textLength)
				{
					var amtRemaining:int = (newLine.textLength - workLine.textLength);
					var nextLine:int = workIndex + 1;
					while (amtRemaining > 0)
					{
						afterLine = _lines[nextLine];
						if (amtRemaining < afterLine.textLength)
						{
							afterLine.setTextLength(afterLine.textLength - amtRemaining);
							afterLine.damage(TextLineValidity.INVALID);
							break;
						}
						else
						{
							deleteCount++;
							amtRemaining -= afterLine.textLength;
							nextLine++;
							afterLine = nextLine < _lines.length ? _lines[nextLine] : null
						}
					}
					if (afterLine && afterLine.absoluteStart != newLine.absoluteStart + newLine.textLength)
					{
						afterLine.setAbsoluteStart(newLine.absoluteStart + newLine.textLength);
						afterLine.damage(TextLineValidity.INVALID);
						CONFIG::debug { assert(afterLine.textLength != 0, "0 width line"); }
					}
					damageStart = newLine.absoluteStart + newLine.textLength;
				}
				// remove userData on the deleted lines so they can be recycled
				if (TextLineRecycler.textLineRecyclerEnabled)
				{
					var backgroundManager:BackgroundManager = textFlow.backgroundManager;
					for (var recycleIdx:int = workIndex; recycleIdx < workIndex+deleteCount; recycleIdx++)
					{
						var textLine:TextLine = TextFlowLine(_lines[recycleIdx]).peekTextLine();
						if (textLine && !textLine.parent)
						{
							// lines shouldn't be valid here but lets check anyhow
							CONFIG::debug { assert(textLine.validity != TextLineValidity.VALID,"caught a bug here"); }
							if (textLine.validity != TextLineValidity.VALID)	// recycle immediately if not parented
							{
								textLine.userData = null;
								TextLineRecycler.addLineForReuse(textLine);
								if (backgroundManager)
									backgroundManager.removeLineFromCache(textLine);
							}
						}
					}
				}
				_lines.splice(workIndex, deleteCount, newLine);
			}
	
			if (_damageAbsoluteStart > damageStart)
				_damageAbsoluteStart = damageStart;
			// CONFIG::debug { debugCheckTextFlowLines(false); }
	//		CONFIG::debug { checkFirstDamaged(); }	 enabling this will cause false positives due to _damageAbsoluteStart during composition not updated	when GEOMETRY_DAMAGE lines are cleared
		}
		
		/** @private - helper function for finding a base swf context from a swf context */
		tlf_internal static function computeBaseSWFContext(context:ISWFContext):ISWFContext
		{
			return context && Object(context).hasOwnProperty("getBaseSWFContext") ? context["getBaseSWFContext"]() : context;
		}
		/** 
		* The ISWFContext instance used to make FTE calls as needed. 
		*
		* <p>By default, the ISWFContext implementation is this FlowComposerBase object.
		* Applications can provide a custom implementation to use fonts
		* embedded in a different SWF file or to cache and reuse text lines.</p>
		* 
		* @see flashx.textLayout.compose.ISWFContext
		* 
		* @playerversion Flash 10
		* @playerversion AIR 1.5
	 	* @langversion 3.0
	 	*/
 	
		public function get swfContext():ISWFContext
		{
			return _swfContext;
		}
		public function set swfContext(context:ISWFContext):void
		{
			if (context != _swfContext)
			{
				// swf contexts can be wrappers for other swf contexts - we're going to let the swfcontext give us a hint here
				if (textFlow)
				{
					var newBaseContext:ISWFContext =  computeBaseSWFContext(context);
					var oldBaseContext:ISWFContext =  computeBaseSWFContext(_swfContext);
	
					_swfContext = context;

					if (newBaseContext != oldBaseContext)
					{
						damage(0,textFlow.textLength,FlowDamageType.INVALID);
						textFlow.invalidateAllFormats();
					}
				}
				else
					_swfContext = context;				
			}
		}
		
		/**
		 * Validate that the lines associated with the flow are internally consistent. 
		 * @private
		 * The start of the next line has to match the start of the previous line + the 
		 * span of the previous line. The last line needs to end at the end of the flow,
		 * and the first line must be at the start of the text.
		 */
		CONFIG::debug public function debugCheckTextFlowLines(validateControllers:Boolean=true):int
		{
			var rslt:int = 0;
			var position:int = 0;
			var overflow:Boolean = false;
			for each (var line:TextFlowLine in _lines)
			{
				// trace("validateLines:",lines.indexOf(line).toString()," ",line.start," ",line.textLength);
				rslt += assert(line.absoluteStart == position, "Line start incorrect");
				rslt += assert(line.textLength >= 0,"Invalind length");
				if (validateControllers)
				{
					var lineController:ContainerController = line.controller;
					if (lineController != null)
					{
						rslt += assert(overflow == false,"non overflow line after overflow line?");
						rslt += assert(line.absoluteStart >= line.controller.absoluteStart,"bad container mapping");
						rslt += assert(line.absoluteStart+line.textLength<= lineController.absoluteStart+lineController.textLength,"bad container mapping");
					}
					else
						overflow = true;
				}
				position += line.textLength;
			}
			return rslt;
		}		
	}
}
