////////////////////////////////////////////////////////////////////////////////
//
// 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.elements
{
	import flash.text.engine.GroupElement;
	import flash.text.engine.TextElement;
	import flash.utils.getQualifiedClassName;
	
	import flashx.textLayout.container.ContainerController;
	import flashx.textLayout.debug.Debugging;
	import flashx.textLayout.debug.assert;
	import flashx.textLayout.events.ModelChange;
	import flashx.textLayout.formats.FormatValue;
	import flashx.textLayout.formats.ITextLayoutFormat;
	import flashx.textLayout.formats.TextLayoutFormat;
	import flashx.textLayout.formats.WhiteSpaceCollapse;
	import flashx.textLayout.property.Property;
	import flashx.textLayout.tlf_internal;
	import flashx.textLayout.utils.CharacterUtil;
	
	use namespace tlf_internal;
			
	[DefaultProperty("mxmlChildren")]
	
	/** 
	* The SpanElement class represents a run of text that has a single set of formatting attributes applied. SpanElement 
	* objects contain the text in a paragraph. A simple paragraph (ParagraphElement) includes one or more SpanElement objects. 
	*
	* <p>A ParagraphElement will have a single SpanElement object if all the text in the paragraph shares the same set of 
	* attributes. It has multiple SpanElement objects if the text in the paragraph has multiple formats.</p>
	*
	* @playerversion Flash 10
	* @playerversion AIR 1.5
	* @langversion 3.0
	*
	* @includeExample examples/SpanElementExample.as -noswf
	*
	* @see FlowElement
	* @see ParagraphElement
	* @see TextFlow
     	*/
     	
	public class SpanElement extends FlowLeafElement
	{	
		/** @private */
		tlf_internal static const kParagraphTerminator:String = '\u2029';
		
		/** Constructor - creates a SpanElement object to hold a run of text in a paragraph.
		*
		* @playerversion Flash 10
		* @playerversion AIR 1.5
	 	* @langversion 3.0
	 	*/
	 	
		public function SpanElement()
		{
			super();
		}
		
		/** @private */
		override tlf_internal function createContentElement():void
		{
			if (_blockElement)
				return;
			
			computedFormat;	// BEFORE creating the element
			_blockElement = new TextElement(_text,null);			
			CONFIG::debug { Debugging.traceFTECall(_blockElement,null,"new TextElement()"); }
			CONFIG::debug { Debugging.traceFTEAssign(_blockElement, "text", _text); }
			super.createContentElement();
		}
		
		/** @private */
		public override function shallowCopy(startPos:int = 0, endPos:int = -1):FlowElement
		{
			if (endPos == -1)
				endPos = textLength;
				
			// Note to callers: If you are calling this function outside a try/catch, do ensure that the 
			// state of the model is coherent before the call.
			var retFlow:SpanElement = super.shallowCopy(startPos, endPos) as SpanElement;
						
			var startSpan:int = 0;
			var endSpan:int = startSpan + textLength;
			
			var leafElStartPos:int = startSpan >= startPos ? startSpan : startPos;
			var leafElEndPos:int =  endSpan < endPos ? endSpan : endPos;
			if (leafElEndPos == textLength && hasParagraphTerminator)
				--leafElEndPos;
				
			if (leafElStartPos > leafElEndPos)
				throw RangeError(GlobalSettings.resourceStringFunction("badShallowCopyRange"));
			
			if (((leafElStartPos != endSpan) && CharacterUtil.isLowSurrogate(_text.charCodeAt(leafElStartPos))) ||
				((leafElEndPos != 0) && CharacterUtil.isHighSurrogate(_text.charCodeAt(leafElEndPos-1))))
					throw RangeError(GlobalSettings.resourceStringFunction("badSurrogatePairCopy"));
			
			if (leafElStartPos != leafElEndPos)
				retFlow.replaceText(0, retFlow.textLength,  _text.substring(leafElStartPos, leafElEndPos));
			
			return retFlow;
		}	
	
		/** @private */
		override protected function get abstract():Boolean
		{ return false; }		
		
		/** @private */
		tlf_internal override function get defaultTypeName():String
		{ return "span"; }

		/** @private */
		public override function get text():String
		{
			// test textLength cause this is a property and the debugger may run this calculation in intermediate states
			if (textLength == 0)
				return "";
			
			return hasParagraphTerminator ? _text.substr(0,textLength-1) : _text;
		}
		/** 
		 * Receives the String of text that this SpanElement object holds.
		 *
		 * <p>The text of a span does not include the carriage return (CR) at the end of the paragraph
		 * but it is included in the value of <code>textLength</code>.</p>
		 *
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
	 	 * @langversion 3.0
	 	 */
	 	
	 	public function set text(textValue:String):void
		{
			//original code stripped breaking and tab characters.  new code moved to collapseWhitevar newLineTabPattern:RegExp = /[\n\r\t]/g;
			replaceText(0,textLength, textValue); 
		} 
		
		/** @private */
		public override function getText(relativeStart:int=0, relativeEnd:int=-1, paragraphSeparator:String="\n"):String
		{
			if (relativeEnd == -1)
				relativeEnd = textLength;
			
			if (textLength && relativeEnd == textLength && hasParagraphTerminator)
				--relativeEnd;		// don't include terminator
			return _text ? _text.substring(relativeStart, relativeEnd) : "";
		}

		[RichTextContent]
		/** 
		 * Sets text based on content within span tags; always deletes existing children.
		 * This property is intended for use during MXML compiled import in Flex. Flash Professional ignores this property.
         * When TLF markup elements have other
		 * TLF markup elements as children, the children are assigned to this property.
		 *
		 * @throws TypeError If array element is not a SpecialCharacterElement or a String.
		 * @param array - an array of elements within span tags. Each element of array must be a SpecialCharacterElement or a String.
		 *
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
	 	 * @langversion 3.0
	 	 */
		public function get mxmlChildren():Array
		{
			return [ text ];
		}
		public function set mxmlChildren(array:Array):void
		{
			/* NOTE: all FlowElement implementers and overrides of mxmlChildren must specify [RichTextContent] metadata */

			var str:String = new String();
			for each (var elem:Object in array)
			{
				if (elem is String)
					str += elem as String;
				else if (elem is Number)	// TODO: remove the Number else if when we can test with the most recent compilers.  The [RichTextContent] metadata fixes the issue
					str += elem.toString();
				else if (elem is BreakElement)
					str += String.fromCharCode(0x2028);
				else if (elem is TabElement)
				{
					// Add a placeholder (from Unicode private use area) instead of '\t' because the latter is 
					// stripped during white space collapse. After white space collapse, we will change the placeholder
					// to '\t' 
					str += String.fromCharCode(0xE000); 
				}	
				else if (elem != null)
					throw new TypeError(GlobalSettings.resourceStringFunction("badMXMLChildrenArgument",[ getQualifiedClassName(elem) ]));	// NO PMD
					
			}
			replaceText(0,textLength, str); 
		}
		
		
		/** 
		 * Specifies whether this SpanElement object terminates the paragraph. The SpanElement object that terminates a 
		 * paragraph has an extra, hidden character at the end. This character is added automatically by the component and is
		 * included in the value of the <code>textLength</code> property.
		 * 
		 * @private */
		 
		tlf_internal function get hasParagraphTerminator():Boolean
		{
			var p:ParagraphElement = getParagraph();
			return (p && p.getLastLeaf() == this); 
		}
		
		/** @private */
		CONFIG::debug tlf_internal function verifyParagraphTerminator():void
		{
			assert(_text && _text.length && _text.charAt(_text.length-1) == SpanElement.kParagraphTerminator,
				"attempting to remove para terminator when it doesn't exist");
		}
		
		
		/**
		 * Makes a shallow copy of this SpanElement between 2 character positions
		 * and returns it as a FlowElement.  Unlike deepCopy, shallowCopy does
		 * not copy any of the children of this SpanElement.
		 * 
		 */
		 
		 // If I have a sequence of different sorts of spaces (e.g., en quad, hair space), would I want them converted down to one space? Probably not.
		 // For now, u0020 is the only space character we consider for eliminating duplicates, though u00A0 (non-breaking space) is potentially eligible. 
		 private static const _dblSpacePattern:RegExp = /[\u0020]{2,}/g;
		 // Tab, line feed, and carriage return
		 private static const _newLineTabPattern:RegExp = /[\u0009\u000a\u000d]/g;
		 private static const _tabPlaceholderPattern:RegExp = /\uE000/g;
		 
		 // static private const anyPrintChar:RegExp = /[^\s]/g;
		 // Consider only tab, line feed, carriage return, and space as characters used for pretty-printing. 
		 // While debatable, this is consistent with what CSS does. 
		 static private const anyPrintChar:RegExp = /[^\u0009\u000a\u000d\u0020]/g; 

		 /** @private */
		tlf_internal override function applyWhiteSpaceCollapse(collapse:String):void
		{
			var ffc:ITextLayoutFormat = this.formatForCascade;
			var wsc:* = ffc ? ffc.whiteSpaceCollapse : undefined;
			if (wsc !== undefined && wsc != FormatValue.INHERIT)
				collapse = wsc;
				
			var origTxt:String = text;
			var tempTxt:String = origTxt;
				
			if (!collapse /* null == default value == COLLAPSE */ || collapse == WhiteSpaceCollapse.COLLAPSE)
			{
				// The span was added automatically when a String was passed to replaceChildren.
				// If it contains only whitespace, we remove the text.
				if (impliedElement && parent != null)
				{
					// var matchArray:Array = tempTxt.search(anyPrintChar);
					if (tempTxt.search(anyPrintChar) == -1)
					{
						parent.removeChild(this);
						return;
					}
				}
				
				// For now, replace the newlines and tabs inside the element with a space.
				// This is necessary for support of compiled mxml files that have newlines and tabs, because
				// these are most likely not intended to be part of the text content, but only there so the
				// text can be conveniently edited in the mxml file. Later on we need to add standalone elements
				// for <br/> and <tab/>. Note that tab character is not supported in HTML.	
				tempTxt = tempTxt.replace(_newLineTabPattern, " ");

				// Replace sequences of 2 or more whitespace characters with single space
				tempTxt = tempTxt.replace(_dblSpacePattern, " ");
			}
			
			// Replace tab placeholders (used for tabs that are expected to survive whitespace collapse) with '\t'
			tempTxt = tempTxt.replace(_tabPlaceholderPattern, '\t')
			if (tempTxt != origTxt)
				replaceText(0, textLength, tempTxt);

			super.applyWhiteSpaceCollapse(collapse);
		}
		
		/** 
		 * Updates the text in text span based on the specified start and end positions. To insert text, set the end position
		 * equal to the start position. To append text to the existing text in the span, set the start position and the
		 * end position equal to the length of the existing text.
		 *
		 * <p>The replaced text includes the start character and up to but not including the end character.</p>
		 * 
		 *  @param relativeStartPosition The index position of the beginning of the text to be replaced, 
		 *   relative to the start of the span. The first character in the span is at position 0.
		 *  @param relativeEndPosition The index one position after the last character of the text to be replaced, 
		 *   relative to the start of the span. Set this value equal to <code>relativeStartPos</code>
		 *   for an insert. 
		 *  @param textValue The replacement text or the text to add, as the case may be.
		 * 
		 *  @throws RangeError The <code>relativeStartPosition</code> or <code>relativeEndPosition</code> specified is out of 
		 * range or a surrogate pair is being split as a result of the replace.
		 *
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
	 	 * @langversion 3.0
	 	 *
	 	 * @includeExample examples/SpanElement_replaceTextExample.as -noswf
		 */
		 
		public function replaceText(relativeStartPosition:int, relativeEndPosition:int, textValue:String):void
		{
			// Note to callers: If you are calling this function outside a try/catch, do ensure that the 
			// state of the model is coherent before the call.
			if (relativeStartPosition < 0 || relativeEndPosition > textLength || relativeEndPosition < relativeStartPosition)
				throw RangeError(GlobalSettings.resourceStringFunction("invalidReplaceTextPositions"));	


			if ((relativeStartPosition != 0 && relativeStartPosition != textLength && CharacterUtil.isLowSurrogate(_text.charCodeAt(relativeStartPosition))) ||
				(relativeEndPosition != 0 && relativeEndPosition != textLength && CharacterUtil.isHighSurrogate(_text.charCodeAt(relativeEndPosition-1))))
					throw RangeError (GlobalSettings.resourceStringFunction("invalidSurrogatePairSplit"));
				
			if (hasParagraphTerminator)
			{
				CONFIG::debug { assert(textLength > 0,"invalid span"); }
				if (relativeStartPosition == textLength)
					relativeStartPosition--;
				if (relativeEndPosition == textLength)
					relativeEndPosition--;
			}
			
			if (relativeEndPosition != relativeStartPosition)
				modelChanged(ModelChange.TEXT_DELETED,this,relativeStartPosition,relativeEndPosition-relativeStartPosition);
			
			replaceTextInternal(relativeStartPosition,relativeEndPosition,textValue);
			
			if (textValue && textValue.length)
				modelChanged(ModelChange.TEXT_INSERTED,this,relativeStartPosition,textValue.length);
		}
		private function replaceTextInternal(startPos:int, endPos:int, textValue:String):void
		{			
			var textValueLength:int = textValue == null ? 0 : textValue.length;
			var deleteTotal:int = endPos-startPos;
			var deltaChars:int =  textValueLength - deleteTotal;
			if (_blockElement)
			{
				(_blockElement as TextElement).replaceText(startPos,endPos,textValue);
				_text = _blockElement.rawText;
				CONFIG::debug { Debugging.traceFTECall(null,_blockElement as TextElement,"replaceText",startPos,endPos,textValue); }
			}
			else if (_text)
			{
				if (textValue)
					_text = _text.slice(0, startPos) + textValue + _text.slice(endPos, _text.length);
				else
					_text = _text.slice(0, startPos) + _text.slice(endPos, _text.length);
			}
			else
				_text = textValue;
			
			if (deltaChars != 0)
			{
				updateLengths(getAbsoluteStart() + startPos, deltaChars, true);
				deleteContainerText(endPos,deleteTotal);
				
				if (textValueLength != 0)
				{
					var enclosingContainer:ContainerController = getEnclosingController(startPos);
					if (enclosingContainer)
						ContainerController(enclosingContainer).setTextLength(enclosingContainer.textLength + textValueLength);
				}
			}

			CONFIG::debug { 
				assert(textLength == (_text ? _text.length : 0),"span textLength doesn't match the length of the text property, text property length is " + _text.length.toString() + " textLength property is " + textLength.toString());
				assert(_blockElement == null || _blockElement.rawText == _text,"mismatched text");
			}
		}
	
		/** @private */
		tlf_internal function addParaTerminator():void
		{
			CONFIG::debug 
			{ 
				// TODO: Is this assert valid? Do we prevent users from adding para terminators themselves? 
				if (_blockElement && _blockElement.rawText && _blockElement.rawText.length)
					assert(_blockElement.rawText.charAt(_blockElement.rawText.length-1) != SpanElement.kParagraphTerminator,"adding para terminator twice");
			}

			replaceTextInternal(textLength,textLength,SpanElement.kParagraphTerminator);
			
			CONFIG::debug 
			{ 
				// TODO: Is this assert valid? Do we prevent users from adding para terminators themselves? 
				if (_blockElement)
					assert(_blockElement.rawText.charAt(_blockElement.rawText.length-1) == SpanElement.kParagraphTerminator,"adding para terminator failed");
			}			
			
			modelChanged(ModelChange.TEXT_INSERTED,this,textLength-1,1);
		}
		/** @private */
		tlf_internal function removeParaTerminator():void
		{
			CONFIG::debug 
			{ 
				assert(_text && _text.length && _text.charAt(_text.length-1) == SpanElement.kParagraphTerminator,
					"attempting to remove para terminator when it doesn't exist");
			}
			replaceTextInternal(textLength-1,textLength,"");
			modelChanged(ModelChange.TEXT_DELETED,this,textLength > 0 ? textLength-1 : 0,1);
		}
		// **************************************** 
		// Begin tree modification support code
		// ****************************************

		/** 
		 * Splits this SpanElement object at the specified position and returns a new SpanElement object for the content
		 * that follows the specified position. 
		 *
		 * <p>This method throws an error if you attempt to split a surrogate pair. In Unicode UTF-16, a surrogate pair is a pair of 
		 * 16-bit code units (a high code unit and a low code unit) that represent one of the abstract Unicode characters 
		 * that cannot be represented in a single 16-bit word. The 16-bit high code unit is in the range of D800 to DBFF. The
		 * 16-bit low code unit is in the range of DC00 to DFFF.</p>
		 * 
		 * @param relativePosition - relative position in the span to create the split
		 * @return - the newly created span. 
		 * @throws RangeError <code>relativePosition</code> is less than 0 or greater than textLength or a surrogate pair is being split.
		 *
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
	 	 * @langversion 3.0
	 	 * 
	 	 * @private
	 	 */
	 	 
		public override function splitAtPosition(relativePosition:int):FlowElement
		{
			// Note to callers: If you are calling this function outside a try/catch, do ensure that the 
			// state of the model is coherent before the call.
			if (relativePosition < 0 || relativePosition > textLength)
				throw RangeError(GlobalSettings.resourceStringFunction("invalidSplitAtPosition"));
			
			if ((relativePosition < textLength) && CharacterUtil.isLowSurrogate(String(text).charCodeAt(relativePosition)))
				throw RangeError (GlobalSettings.resourceStringFunction("invalidSurrogatePairSplit"));
			
			var newSpan:SpanElement = new SpanElement();
			// clone styling information
			newSpan.id = this.id;
			newSpan.typeName = this.typeName;			
			
			if (parent)
			{
				var newBlockElement:TextElement;
				var newSpanLength:int = textLength - relativePosition;
				if (_blockElement)
				{
					// optimized version leverages player APIs
					// TODO: Jeff to add split on TextElement so we don't have to go find a group every time
					var group:GroupElement = parent.createContentAsGroup();
					
					var elementIndex:int = group.getElementIndex(_blockElement);
					
					CONFIG::debug { assert(elementIndex == parent.getChildIndex(this),"bad group index"); }
					CONFIG::debug { assert(elementIndex != -1 && elementIndex < group.elementCount,"bad span split"); }
					//trace("GROUP BEFORE: " + group.rawText);
					//trace("BLOCK BEFORE: " + group.block.content.rawText);
					//trace("calling group.splitTextElement("+elementIndex.toString()+","+relativePosition.toString()+")");
					group.splitTextElement(elementIndex, relativePosition);
					CONFIG::debug { Debugging.traceFTECall(null,group,"splitTextElement",elementIndex,relativePosition); }

					//trace("GROUP AFTER: " + group.rawText);
					//trace("BLOCK AFTER: " + group.block.content.rawText);
					
					// no guarantee on how the split works
					_blockElement = group.getElementAt(elementIndex);
					_text = _blockElement.rawText;
					CONFIG::debug { Debugging.traceFTECall(_blockElement,group,"getElementAt",elementIndex); }
					newBlockElement = group.getElementAt(elementIndex+1) as TextElement;
					CONFIG::debug { Debugging.traceFTECall(newBlockElement,group,"getElementAt",elementIndex+1); }
				}
				else if (relativePosition < textLength)
				{
					newSpan.text = _text.substr(relativePosition);
					_text = _text.substring(0, relativePosition);
				}

				// Split this span at the offset, into two equivalent spans
				modelChanged(ModelChange.TEXT_DELETED,this,relativePosition,newSpanLength);
				newSpan.quickInitializeForSplit(this, newSpanLength, newBlockElement);

				setTextLength(relativePosition);
			
				// slices it in, sets the parent and the start
				parent.addChildAfterInternal(this,newSpan);	
				
				var p:ParagraphElement = this.getParagraph();
				p.updateTerminatorSpan(this,newSpan);
				
				parent.modelChanged(ModelChange.ELEMENT_ADDED,newSpan,newSpan.parentRelativeStart,newSpan.textLength);
			}
			else
			{
				// this version also works if para is non-null but may not be as efficient.
				newSpan.format = format;

				// could be we are splitting 
				if (relativePosition < textLength)
				{
					newSpan.text = String(this.text).substr(relativePosition);
					replaceText(relativePosition,textLength,null);
				}
			}
			
			return newSpan;
		}
		
		/** @private */
		tlf_internal override function normalizeRange(normalizeStart:uint,normalizeEnd:uint):void
		{
			if (this.textLength == 1 && !bindableElement)
			{
				var p:ParagraphElement = getParagraph();
				if (p && p.getLastLeaf() == this)
				{
					var prevLeaf:FlowLeafElement = getPreviousLeaf(p);
					if (prevLeaf)
					{
						if (!TextLayoutFormat.isEqual(this.format, prevLeaf.format))
							this.format = prevLeaf.format;
					}
				}
			}
			super.normalizeRange(normalizeStart,normalizeEnd);
		}

		/** @private */
		tlf_internal override function mergeToPreviousIfPossible():Boolean
		{
			if (parent && !bindableElement)
			{
				var myidx:int = parent.getChildIndex(this);
				if (myidx != 0)
				{
					var sib:SpanElement = parent.getChildAt(myidx-1) as SpanElement;
					
					// If the element we're checking for merge has only the terminator, and the previous element
					// is not a Span, then we always merge with the previous span (NOT the previous sib). 
					// We just remove this span, and add the terminator to the previous span.
					if (!sib && this.textLength == 1 && this.hasParagraphTerminator)
					{
						var p:ParagraphElement = getParagraph();
						if (p)
						{
							var prevLeaf:FlowLeafElement = getPreviousLeaf(p) as SpanElement;
							if (prevLeaf)
							{
								parent.removeChildAt(myidx);
								return true;
							}
						}
					}

					if (sib == null)
						return false;
					
					
					// If this has an active event mirror do not merge
					if (this.hasActiveEventMirror())
						return false;
					var thisIsSimpleTerminator:Boolean = textLength == 1 && hasParagraphTerminator;
					// if sib has an active event mirror still merge if this is a simple terminator span
					if (sib.hasActiveEventMirror() && !thisIsSimpleTerminator)
						return false;
					
					// always merge if this is just a terminator
					if (thisIsSimpleTerminator || equalStylesForMerge(sib))
					{
						CONFIG::debug { assert(this.parent == sib.parent, "Should never merge two spans with different parents!"); }
						CONFIG::debug { assert(TextLayoutFormat.isEqual(this.formatForCascade,sib.formatForCascade) || (this.textLength == 1 && this.hasParagraphTerminator), "Bad merge!"); }

						// Merge the spans
						var siblingInsertPosition:int = sib.textLength;
						sib.replaceText(siblingInsertPosition, siblingInsertPosition, this.text);
						parent.removeChildAt(myidx);
						return true;
					}
				}
			} 
			return false;
		}
		
		// **************************************** 
		// Begin debug support code
		// ****************************************	
		
		/** @private */
		CONFIG::debug public override function debugCheckFlowElement(depth:int = 0, extraData:String = ""):int
		{
			// debugging function that asserts if the flow element tree is in an invalid state
			
			var rslt:int = super.debugCheckFlowElement(depth,"text:"+String(text).substr(0,32)+" "+extraData);

			assert(_blockElement == null || _blockElement.rawText == _text,"debugCheckFlowElement: mismatched text");
			var textLen:int = textLength;
			if (_text)
				rslt += assert(textLen == _text.length,"span is different than its textElement, span text length is " + _text.length.toString() + " expecting " + textLen.toString());
			else	
				rslt += assert(textLen == 0,"span is different than its textElement, span text length is null expecting " + textLen.toString());
			rslt += assert(this != getParagraph().getLastLeaf() || (_text.length >= 1 && _text.substr(_text.length-1,1) == SpanElement.kParagraphTerminator),"last span in paragraph must end with terminator");
			return rslt;
		}
	}
}
