////////////////////////////////////////////////////////////////////////////////
//
// 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.conversion 
{
	import flash.system.System;
	
	import flashx.textLayout.TextLayoutVersion;
	import flashx.textLayout.debug.assert;
	import flashx.textLayout.elements.BreakElement;
	import flashx.textLayout.elements.Configuration;
	import flashx.textLayout.elements.ContainerFormattedElement;
	import flashx.textLayout.elements.FlowElement;
	import flashx.textLayout.elements.FlowGroupElement;
	import flashx.textLayout.elements.FlowLeafElement;
	import flashx.textLayout.elements.GlobalSettings;
	import flashx.textLayout.elements.IConfiguration;
	import flashx.textLayout.elements.ListElement;
	import flashx.textLayout.elements.ListItemElement;
	import flashx.textLayout.elements.ParagraphElement;
	import flashx.textLayout.elements.ParagraphFormattedElement;
	import flashx.textLayout.elements.SpanElement;
	import flashx.textLayout.elements.TabElement;
	import flashx.textLayout.elements.TextFlow;
	import flashx.textLayout.property.Property;
	import flashx.textLayout.tlf_internal;
	use namespace tlf_internal;

	[ExcludeClass]
	/**
	 * @private  
	 * BaseTextLayoutImporter is a base class for handling the import/export of TextLayout text in the native format.
	 */ 
	internal class BaseTextLayoutImporter extends ConverterBase implements ITextImporter
	{	
		private var _ns:Namespace;		// namespace of expected in imported/exported content
		
		private var _textFlowNamespace:Namespace; // namespace of the TextFlow element against which the namespaces of the following elements are validated
				
		protected var _config:ImportExportConfiguration;
		protected var _textFlowConfiguration:IConfiguration = null;
		protected var _importVersion:uint;
		
		// 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; 

		public function BaseTextLayoutImporter(nsValue:Namespace, config:ImportExportConfiguration)
		{
			_ns = nsValue;
			_config = config;
		}
		
		tlf_internal override function clear():void
		{
			super.clear();
			_textFlowNamespace = null;
			_impliedPara = null;
		}
		
		/** @copy ITextImporter#importToFlow()
		 */
		public function importToFlow(source:Object):TextFlow
		{
			clear();		// empty results of previous imports
			
			if (throwOnError)
				return importToFlowCanThrow(source);
			
			var rslt:TextFlow = null;
			var savedErrorHandler:Function = Property.errorHandler;
			try
			{
				Property.errorHandler = importPropertyErrorHandler;
				rslt = importToFlowCanThrow(source);
			}
			catch (e:Error)
			{
				reportError(e.toString());
			}
			Property.errorHandler = savedErrorHandler;
			return rslt;
		}
		
		/** @copy ITextImporter#get configuration()
		 */
		public function get configuration():IConfiguration
		{
			return _textFlowConfiguration;
		}
		
		public function set configuration(value:IConfiguration):void
		{
			_textFlowConfiguration = value;
		}

		/** @private */
		protected function importPropertyErrorHandler(p:Property,value:Object):void
		{
			reportError(Property.createErrorString(p,value));
		}
		
		private function importToFlowCanThrow(source:Object):TextFlow
		{
			if (source is String)
				return importFromString(String(source));
			else if (source is XML)
				return importFromXML(XML(source));
			return null;
		}
		
		/** Parse and convert input data.
		 * 
		 * @param source - a string which is in XFL format. String is applied to an XML object then passed
		 * to importFromXML to be processed.  The source must be capable of being cast as an XML
		 * object (E4X). 
		 */
		protected function importFromString(source:String):TextFlow
		{
			var originalSettings:Object = XML.settings();
			try
			{
				XML.ignoreProcessingInstructions = false;		
				XML.ignoreWhitespace = false;
				var xmlTree:XML = new XML(source);				
			}			
			finally
			{
				XML.setSettings(originalSettings);
			}	
			
			var textFlow:TextFlow = importFromXML(xmlTree);
			if (Configuration.playerEnablesArgoFeatures)
				System["disposeXML"](xmlTree);
			return textFlow;
		}
		
		/** Parse and convert input data.
		 * 
		 * xflSource is a XFL formated object which must be capable of being cast as an XML
		 * object (E4X). 
		 */
		protected function importFromXML(xmlSource:XML):TextFlow
			// Parse an XFL hierarchy into a TextFlow, using the geometry supplied by a TextFrame
			// to host child containers (e.g. tables). This is the main entry point into this class.
		{
			return parseContent(xmlSource[0]);
		}
		
		// This routine imports a TextFlow
		protected function parseContent(rootStory:XML):TextFlow
		{
			// If the root element isn't a textFlow we know how to parse, keep descending the hierarchy.
			var child:XML = rootStory..*::TextFlow[0];
			if (child)
				return parseTextFlow(this, rootStory);
			return null;
		}
		
		/** Returns the namespace used in for writing XML/XFL
		 * 
		 * @return the Namespace being used.
		 */
		public function get ns(): Namespace
		{
			return _ns;
		}
		
		// Remove double spaces, tabs, and newlines.
		// 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. 
		static private const dblSpacePattern:RegExp = /[\u0020]{2,}/g;
		// Tab, line feed, and carriage return
		static private const tabNewLinePattern:RegExp = /[\u0009\u000a\u000d]/g;
		protected static function stripWhitespace(insertString:String):String
		{
			// Replace the newlines and tabs inside the element with spaces.
			return insertString.replace(tabNewLinePattern, " ");
		}

		/** Parse XML and convert to  TextFlow. 
		 * @param importer		parser object
		 * @param xmlToParse	content to parse
		 * @param parent always null - this parameter is only provided to match FlowElementInfo.importer signature
		 * @return TextFlow	the new TextFlow created as a result of the parse
		 */
		static public function parseTextFlow(importer:BaseTextLayoutImporter, xmlToParse:XML, parent:Object=null):TextFlow
		{
			return importer.createTextFlowFromXML(xmlToParse, null);
		}		
		
		/** Static method to parse the supplied XML into a paragrph. Parse the <p ...> tag and it's children.
		 * 
		 * @param importer	parser object
		 * @param xmlToParse	content to parse
		 * @param parent 		the parent for the new content
		 */
		static public function parsePara(importer:BaseTextLayoutImporter, xmlToParse:XML, parent:FlowGroupElement):void
		{
			var paraElem:ParagraphElement = importer.createParagraphFromXML(xmlToParse);
			if (importer.addChild(parent, paraElem))
			{
				importer.parseFlowGroupElementChildren(xmlToParse, paraElem);
				//if parsing an empty paragraph, create a Span for it.
				if (paraElem.numChildren == 0)
					paraElem.addChild(new SpanElement());
			}
		}
		
		static protected function copyAllStyleProps(dst:FlowLeafElement,src:FlowLeafElement):void
		{
			dst.format = src.format;
			dst.typeName	= src.typeName;
			dst.id          = src.id;
		}
		
		/** Static method for constructing a span from XML. Parse the <span> ... </span> tag. 
		 * Insert the span into its parent
		 * 
		 * @param importer	parser object
		 * @param xmlToParse	content to parse
		 * @param parent 		the parent for the new content
		 */
		static public function parseSpan(importer:BaseTextLayoutImporter, xmlToParse:XML, parent:FlowGroupElement):void
		{
			var firstSpan:SpanElement = importer.createSpanFromXML(xmlToParse);
			
			var elemList:XMLList = xmlToParse[0].children();
			if(elemList.length() == 0)
			{
				// Empty span, but may have formatting, so don't strip it out. 
				// Note: the normalizer may yet strip it out if it is not the last child, but that's the normalizer's business.
				importer.addChild(parent, firstSpan); 
				return;
			}
	
			for each (var child:XML in elemList) 
			{
				var elemName:String = child.name() ? child.name().localName : null;
					
				if (elemName == null) // span text
				{
					if (firstSpan.parent == null)	// hasn't been used yet
					{
						firstSpan.text = child.toString();
						importer.addChild(parent, firstSpan);
					}
					else
					{
						var s:SpanElement = new SpanElement();	// No PMD
						copyAllStyleProps(s,firstSpan);
						s.text = child.toString();
						importer.addChild(parent, s);
					}
				}
				else if (elemName == "br")
				{
					var brElem:BreakElement = importer.createBreakFromXML(child);	// may be null
					if (brElem)
					{
						copyAllStyleProps(brElem,firstSpan);
						importer.addChild(parent, brElem);
					}
					else
						importer.reportError(GlobalSettings.resourceStringFunction("unexpectedXMLElementInSpan",[ elemName ]));
				}
				else if (elemName == "tab")
				{
					var tabElem:TabElement = importer.createTabFromXML(child);	// may be null
					if (tabElem)
					{
						copyAllStyleProps(tabElem,firstSpan);
						importer.addChild(parent, tabElem);
					}
					else
						importer.reportError(GlobalSettings.resourceStringFunction("unexpectedXMLElementInSpan",[ elemName ]));
				}
				else
					importer.reportError(GlobalSettings.resourceStringFunction("unexpectedXMLElementInSpan",[ elemName ]));				
			}
		}
		
		/** Static method for constructing a break element from XML. Validate the <br> ... </br> tag. 
		 * Use "\u2028" as the text; Insert the new element into its parent 
		 * 
		 * @param importer	parser object
		 * @param xmlToParse	content to parse
		 * @param parent 		the parent for the new content
		 */
		static public function parseBreak(importer:BaseTextLayoutImporter, xmlToParse:XML, parent:FlowGroupElement):void
		{
			var breakElem:BreakElement = importer.createBreakFromXML(xmlToParse);
			importer.addChild(parent, breakElem);
		}

		
		/** Static method for constructing a tab element from XML. Validate the <tab> ... </tab> tag. 
		 * Use "\t" as the text; Insert the new element into its parent 
		 * 
		 * @param importer	parser object
		 * @param xmlToParse	content to parse
		 * @param parent 		the parent for the new content
		 */
		static public function parseTab(importer:BaseTextLayoutImporter, xmlToParse:XML, parent:FlowGroupElement):void
		{
			var tabElem:TabElement = importer.createTabFromXML(xmlToParse);	// may be null
			if (tabElem)
				importer.addChild(parent, tabElem);
		}

		static public function parseList(importer:BaseTextLayoutImporter, xmlToParse:XML, parent:FlowGroupElement):void
		{
			var listElem:ListElement = importer.createListFromXML(xmlToParse);
			if (importer.addChild(parent, listElem))
			{
				importer.parseFlowGroupElementChildren(xmlToParse, listElem);
			}
		}

		static public function parseListItem(importer:BaseTextLayoutImporter, xmlToParse:XML, parent:FlowGroupElement):void
		{
			var listItem:ListItemElement = importer.createListItemFromXML(xmlToParse);
			if (importer.addChild(parent, listItem))
			{
				importer.parseFlowGroupElementChildren(xmlToParse, listItem);
				//if parsing an empty list item, create a Paragraph for it.
				if (listItem.numChildren == 0)
					listItem.addChild(new ParagraphElement());	
			}
		}
		
		protected function checkNamespace(xmlToParse:XML):Boolean
		{
			var elementNS:Namespace = xmlToParse.namespace();
			if (!_textFlowNamespace) // Not set yet; must be parsing the TextFlow element
			{
				// TextFlow element: allow only empty namespace and flow namespace
				if (elementNS != ns) 
				{
					reportError(GlobalSettings.resourceStringFunction("unexpectedNamespace", [elementNS.toString()]));
					return false;
				}
				_textFlowNamespace = elementNS;
			}
			// Other elements: must match the namespace of the TextFlow element
			// Specifically, can't be empty unless the TextFlow element's namespace is also empty 
			else if (elementNS != _textFlowNamespace) 
			{
				reportError(GlobalSettings.resourceStringFunction("unexpectedNamespace", [elementNS.toString()]));
				return false;
			}
			
			return true;
		}
		
		public function parseAttributes(xmlToParse:XML,formatImporters:Array):void
		{
			var importer:IFormatImporter;
			// reset them all
			for each (importer in formatImporters)
				importer.reset();
				
			if (!xmlToParse)
				return;
			
			for each (var item:XML in xmlToParse.attributes())
			{
				var propertyName:String = item.name().localName;
				var propertyValue:String = item.toString();
				var imported:Boolean = false;
				
				// Strip out padding properties from XML coming in before TLF 2.0, since they were ignored but are no longer. This preserves the look of the text.
				if (xmlToParse.localName() == "TextFlow")
				{
					if (propertyName == "version")	// skip over the version attribute, we've already processed it
						continue;
				}
				else if (_importVersion < TextLayoutVersion.VERSION_2_0 &&
					 (propertyName == "paddingLeft" || propertyName == "paddingTop" || propertyName == "paddingRight" || propertyName == "paddingBottom"))
					continue;
				for each (importer in formatImporters)
				{
					if (importer.importOneFormat(propertyName,propertyValue))
					{
						imported = true;
						break;
					}
				}
				if (!imported)	// not a supported attribute
					handleUnknownAttribute (xmlToParse.name().localName, propertyName);
			}
		}
				
		static protected function extractAttributesHelper(curAttrs:Object, importer:TLFormatImporter):Object
		{
			if (curAttrs == null)
				return importer.result;
			
			if (importer.result == null)
				return curAttrs;
				
			var workAttrs:Object = new importer.classType(curAttrs);
			workAttrs.apply(importer.result);
			return workAttrs;
		}	
		
		/** Parse XML and convert to  TextFlow.
		 * @param xmlToParse	content to parse
		 * @param textFlow 		TextFlow we're parsing. If null, create or find a new TextFlow based on XML content
		 * @return TextFlow	the new TextFlow created as a result of the parse
		 */
		public function createTextFlowFromXML(xmlToParse:XML, newFlow:TextFlow = null):TextFlow	// No PMD
		{
			CONFIG::debug { assert(false,"missing override for createTextFlowFromXML"); }
			return null;
		}
		
		public function createParagraphFromXML(xmlToParse:XML):ParagraphElement	// No PMD
		{
			CONFIG::debug { assert(false,"missing override for createParagraphFromXML"); }
			return null;
		}
		
		public function createSpanFromXML(xmlToParse:XML):SpanElement	// No PMD
		{
			CONFIG::debug { assert(false,"missing override for createSpanFromXML"); }
			return null;
		}
		
		public function createBreakFromXML(xmlToParse:XML):BreakElement
		{
			parseAttributes(xmlToParse,null);	// no attributes allowed - reports errors
			return new BreakElement();
		}
		
		public function createListFromXML(xmlToParse:XML):ListElement	// No PMD
		{
			CONFIG::debug { assert(false,"missing override for createListFromXML"); }
			return null;
		}

		public function createListItemFromXML(xmlToParse:XML):ListItemElement	// No PMD
		{
			CONFIG::debug { assert(false,"missing override for createListItemFromXML"); }
			return null;
		}
		
		public function createTabFromXML(xmlToParse:XML):TabElement
		{
			parseAttributes(xmlToParse,null);	// reports errors
			return new TabElement();
		}
		
		/** Parse XML, convert to FlowElements and add to the parent.
		 * @param xmlToParse	content to parse
		 * @param parent 		the parent for the new content
		 */
		public function parseFlowChildren(xmlToParse:XML, parent:FlowGroupElement):void
		{
			parseFlowGroupElementChildren(xmlToParse, parent);
		}
		
		/** Parse XML, convert to FlowElements and add to the parent.
		 * @param xmlToParse	content to parse
		 * @param parent 		the parent for the new content
		 * @param chainedParent whether parent actually corresponds to xmlToParse or has been chained (such as when xmlToParse is a formatting element) 
		 */
		public function parseFlowGroupElementChildren(xmlToParse:XML, parent:FlowGroupElement, exceptionElements:Object = null, chainedParent:Boolean=false):void
		{
			for each (var child:XML in xmlToParse.children())
			{
				if (child.nodeKind() == "element")
				{
					parseObject(child.name().localName, child, parent, exceptionElements);
				}
				// look for mixed content here
 				else if (child.nodeKind() == "text") 
 				{
 					var txt:String = child.toString();
 					// Strip whitespace-only text appearing as a child of a container-formatted element
 					var strip:Boolean = false;
 					if (parent is ContainerFormattedElement)
 					{
						strip = txt.search(anyPrintChar) == -1;
					}
					
 					if (!strip) 
						addChild(parent, createImpliedSpan(txt));
				}
			}
			
			// no implied paragraph should extend across container elements
			if (!chainedParent && parent is ContainerFormattedElement)
				resetImpliedPara();
		}
		
		/** create an implied span with specified text */
		public function createImpliedSpan(text:String):SpanElement
		{
			var span:SpanElement = new SpanElement();	// No PMD
			span.text = text;
			return span;
		}
			
		public function createParagraphFlowFromXML(xmlToParse:XML, newFlow:TextFlow = null):TextFlow	// No PMD
		{
			CONFIG::debug { assert(false,"missing override for createParagraphFlowFromXML"); }	// client must override
			return null;
		}
		
		tlf_internal function parseObject(name:String, xmlToParse:XML, parent:FlowGroupElement, exceptionElements:Object=null):void
		{
			if (!checkNamespace(xmlToParse))
				return;

			var info:FlowElementInfo = _config.lookup(name);
			if (!info)
			{
				if (exceptionElements == null || exceptionElements[name] === undefined)
					handleUnknownElement (name, xmlToParse, parent);		
			}
			else
				info.parser(this, xmlToParse, parent);
		}
		
		protected function handleUnknownElement(name:String, xmlToParse:XML, parent:FlowGroupElement):void
		{
			reportError(GlobalSettings.resourceStringFunction("unknownElement", [ name ]));	
		}
		
		protected function handleUnknownAttribute(elementName:String, propertyName:String):void
		{
			reportError(GlobalSettings.resourceStringFunction("unknownAttribute", [ propertyName, elementName ]));	
		}
		
		protected function getElementInfo(xmlToParse:XML):FlowElementInfo
		{
			return _config.lookup(xmlToParse.name().localName);
		}
		
		protected function GetClass(xmlToParse:XML):Class
		{
			var info:FlowElementInfo = _config.lookup(xmlToParse.name().localName);
			return info ? info.flowClass : null;
		}
		
		// In the text model, non-FlowParagraphElements (i.e. spans, images, links, TCY) cannot be children of a ContainerElement (TextFlow, Div etc.)
		// They can only be children of paragraphs or subparagraph blocks. 
		// In XML, however, <p> elements can be implied (for example, a <span> may appear as a direct child of <flow>).  
		// So, while parsing the XML, if we enounter a non-FlowParagraphElement child of a ContainerElement 
		// 1. an explicitly created paragraph is used as the parent instead
		// 2. such explicitly created paragraphs are shared by adjacent flow elements provided there isn't an intervening FlowParagraphElement		
		private var _impliedPara:ParagraphElement = null; 
		
		/** @private */
		tlf_internal function createImpliedParagraph():ParagraphElement
		{
			return createParagraphFromXML(<p/>);
		}
		
		/**
		 * @private
		 * Helper function for adding a child flow element that honors throwOnError setting and uses the parent override
		 * NOTE: You MUST NOT call addChild directly unless you are sure
		 * - There is not possibility of an implied paragraph, and
		 * - Parent is of type that can contain child
		*/
		tlf_internal function addChild(parent:FlowGroupElement, child:FlowElement):Boolean
		{
			if (child is ParagraphFormattedElement)
			{
				// Reset due to possibly intervening FlowParagrahElement; See note 2. above
				resetImpliedPara();
			}
			else if (parent is ContainerFormattedElement)
			{
				// See note 1. above
				if (!_impliedPara)
				{
					// Derived classes may have special behavior for <p> tags. Implied paragraphs may need the same behavior.
					// So call createParagraphFromXML, don't just instantiate a ParagraphElement 
					_impliedPara = createImpliedParagraph();
					parent.addChild(_impliedPara);
				}
				
				parent = _impliedPara;
			}	
			
			if (throwOnError)
				parent.addChild(child);
			else
			{
				try
				{
					parent.addChild(child);
				}
				catch (e:*)
				{
					reportError(e); 	
					return false;
				}
			}
			
			return true;
		} 
		
		tlf_internal function resetImpliedPara():void
		{
			if (_impliedPara)
			{
				onResetImpliedPara(_impliedPara);
				_impliedPara = null;
			}
		}
		
		protected function onResetImpliedPara(para:ParagraphElement):void
		{
		}
	}
}

