////////////////////////////////////////////////////////////////////////////////
//
// 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.property
{
	import flashx.textLayout.debug.assert;
	import flashx.textLayout.elements.GlobalSettings;
	import flashx.textLayout.formats.FormatValue;
	import flashx.textLayout.formats.ListMarkerFormat;
	import flashx.textLayout.formats.TextLayoutFormat;
	import flashx.textLayout.tlf_internal;
	
	use namespace tlf_internal;
		
	[ExcludeClass]
	/** Base class of property metadata.  Each property in the various TextLayout attributes structures has a metadata singletion Property class instance.  The instance
	 * can be used to process the property to and from xml, find out range information and help with the attribute cascade.  The Property class also contains static functions
	 * for processing all the properties collected in a TextLayout Format object. @private  */
	public class Property
	{		
		public static var errorHandler:Function = defaultErrorHandler;
		public static function defaultErrorHandler(p:Property,value:Object):void
		{
			throw(new RangeError(createErrorString(p,value)));
		}
		public static function createErrorString(p:Property,value:Object):String
		{
			return GlobalSettings.resourceStringFunction("badPropertyValue",[ p.name, value.toString() ])
		}
		
		// shared propertyHandler instances
		/** @private */
		tlf_internal static const sharedStringHandler:StringPropertyHandler = new StringPropertyHandler();
		/** @private */
		tlf_internal static const sharedInheritEnumHandler:EnumPropertyHandler = new EnumPropertyHandler([ FormatValue.INHERIT ]);
		/** @private */
		tlf_internal static const sharedUndefinedHandler:UndefinedPropertyHandler = new UndefinedPropertyHandler();
		/** @private */
		tlf_internal static const sharedUintHandler:UintPropertyHandler = new UintPropertyHandler();
		/** @private */
		tlf_internal static const sharedBooleanHandler:BooleanPropertyHandler = new BooleanPropertyHandler();
		
		/** @private */
		tlf_internal static const sharedTextLayoutFormatHandler:FormatPropertyHandler = new FormatPropertyHandler();
		/** @private */
		tlf_internal static const sharedListMarkerFormatHandler:FormatPropertyHandler = new FormatPropertyHandler();
		
		public static function NewBooleanProperty(nameValue:String, defaultValue:Boolean, inherited:Boolean, categories:Vector.<String>):Property
		{			
			var rslt:Property = new Property(nameValue, defaultValue, inherited, categories);
			rslt.addHandlers(sharedUndefinedHandler,sharedBooleanHandler,sharedInheritEnumHandler);
			return rslt;
		}
		
		public static function NewStringProperty(nameValue:String, defaultValue:String, inherited:Boolean, categories:Vector.<String>):Property
		{
			var rslt:Property = new Property(nameValue, defaultValue, inherited, categories);
			rslt.addHandlers(sharedUndefinedHandler,sharedStringHandler);
			return rslt;			
		}
		public static function NewUintProperty(nameValue:String, defaultValue:uint, inherited:Boolean, categories:Vector.<String>):Property
		{
			var rslt:Property = new Property(nameValue, defaultValue, inherited, categories);
			rslt.addHandlers(sharedUndefinedHandler,sharedUintHandler,sharedInheritEnumHandler);
			return rslt;
		}

		public static function NewEnumStringProperty(nameValue:String, defaultValue:String, inherited:Boolean, categories:Vector.<String>, ... rest):Property
		{
			var rslt:Property = new Property(nameValue, defaultValue, inherited, categories);
			rslt.addHandlers(sharedUndefinedHandler,new EnumPropertyHandler(rest),sharedInheritEnumHandler);
			return rslt;
		}
		
		public static function NewIntOrEnumProperty(nameValue:String, defaultValue:Object, inherited:Boolean, categories:Vector.<String>, minValue:int, maxValue:int, ... rest):Property
		{		
			var rslt:Property = new Property(nameValue, defaultValue, inherited, categories);
			rslt.addHandlers(sharedUndefinedHandler,new EnumPropertyHandler(rest),new IntPropertyHandler(minValue,maxValue),sharedInheritEnumHandler);
			return rslt;
		}
		
		public static function NewUintOrEnumProperty(nameValue:String, defaultValue:Object, inherited:Boolean, categories:Vector.<String>,  ... rest):Property
		{			
			var rslt:Property = new Property(nameValue, defaultValue, inherited, categories);
			rslt.addHandlers(sharedUndefinedHandler,new EnumPropertyHandler(rest),sharedUintHandler,sharedInheritEnumHandler);
			return rslt;
		}

		public static function NewNumberProperty(nameValue:String, defaultValue:Number, inherited:Boolean, categories:Vector.<String>, minValue:Number, maxValue:Number):Property
		{
			var rslt:Property = new Property(nameValue, defaultValue, inherited, categories);
			rslt.addHandlers(sharedUndefinedHandler,new NumberPropertyHandler(minValue,maxValue),sharedInheritEnumHandler);
			return rslt;
		}
		public static function 	NewNumberOrPercentOrEnumProperty(nameValue:String, defaultValue:Object, inherited:Boolean, categories:Vector.<String>, minValue:Number, maxValue:Number, minPercentValue:String, maxPercentValue:String, ... rest):Property
		{
			var rslt:Property = new Property(nameValue, defaultValue, inherited, categories);
			rslt.addHandlers(sharedUndefinedHandler,new EnumPropertyHandler(rest),new PercentPropertyHandler(minPercentValue,maxPercentValue),new NumberPropertyHandler(minValue,maxValue),sharedInheritEnumHandler);
			return rslt;
		}
		public static function NewNumberOrPercentProperty(nameValue:String, defaultValue:Object, inherited:Boolean, categories:Vector.<String>, minValue:Number, maxValue:Number, minPercentValue:String, maxPercentValue:String):Property
		{			
			var rslt:Property = new Property(nameValue, defaultValue, inherited, categories);
			rslt.addHandlers(sharedUndefinedHandler,new PercentPropertyHandler(minPercentValue,maxPercentValue),new NumberPropertyHandler(minValue,maxValue),sharedInheritEnumHandler);
			return rslt;
		}
		public static function NewNumberOrEnumProperty(nameValue:String, defaultValue:Object, inherited:Boolean, categories:Vector.<String>, minValue:Number, maxValue:Number, ... rest):Property
		{			
			var rslt:Property = new Property(nameValue, defaultValue, inherited, categories);
			rslt.addHandlers(sharedUndefinedHandler,new EnumPropertyHandler(rest),new NumberPropertyHandler(minValue,maxValue),sharedInheritEnumHandler);
			return rslt;
		}
		public static function NewTabStopsProperty(nameValue:String, defaultValue:Array, inherited:Boolean, categories:Vector.<String>):Property
		{
			return new TabStopsProperty(nameValue,defaultValue,inherited,categories);
		}
		public static function NewSpacingLimitProperty(nameValue:String, defaultValue:Object, inherited:Boolean, categories:Vector.<String>, minPercentValue:String, maxPercentValue:String):Property
		{
			var rslt:Property = new Property(nameValue,defaultValue,inherited,categories);
			rslt.addHandlers(sharedUndefinedHandler, new SpacingLimitPropertyHandler(minPercentValue, maxPercentValue), sharedInheritEnumHandler);
			return rslt;
		}
		
		private static const undefinedValue:* = undefined;
		
		public static function NewTextLayoutFormatProperty(nameValue:String, defaultValue:Object, inherited:Boolean, categories:Vector.<String>):Property
		{
			// passing undefined as a value seems to confuse the compiler
			var rslt:Property = new Property(nameValue, undefinedValue, inherited, categories);
			rslt.addHandlers(sharedUndefinedHandler,sharedTextLayoutFormatHandler,sharedInheritEnumHandler);
			return rslt;
		}
		public static function NewListMarkerFormatProperty(nameValue:String, defaultValue:Object, inherited:Boolean, categories:Vector.<String>):Property
		{
			// passing undefined as a value seems to confuse the compiler
			var rslt:Property = new Property(nameValue, undefinedValue, inherited, categories);
			rslt.addHandlers(sharedUndefinedHandler,sharedListMarkerFormatHandler,sharedInheritEnumHandler);
			return rslt;
		}		
		/** not yet enabled.  @private */
		public static const NO_LIMITS:String ="noLimits";
		/** not yet enabled.  @private */
		public static const LOWER_LIMIT:String ="lowerLimit";
		/** not yet enabled.  @private */
		public static const UPPER_LIMIT:String = "upperLimit";
		/** not yet enabled.  @private */
		public static const ALL_LIMITS:String = "allLimits";
		
		// storing name here is redundant but is more efficient 
		private var _name:String;
		private var _default:*;
		private var _inherited:Boolean;
		private var _categories:Vector.<String>;
		private var _hasCustomExporterHandler:Boolean;
		private var _numberPropertyHandler:NumberPropertyHandler;
		
		protected var _handlers:Vector.<PropertyHandler>;
		
		/** Initializer.  Each property has a name and a default. */
		public function Property(nameValue:String,defaultValue:*,inherited:Boolean,categories:Vector.<String>)
		{ 
			_name = nameValue;
			_default = defaultValue;
			_inherited = inherited;
			_categories = categories;
			_hasCustomExporterHandler = false;
		}
		
		/** The name of the property */
		public function get name():String
		{ return _name; }
		
		/** The default value of this property */
		public function get defaultValue():*
		{ return _default; }
		
		/** Is this property inherited */
		public function get inherited():Object
		{ return _inherited; }
		
		/** First listed Category of this property. This is the legacy category from when Properties could only be in one category.  */
		public function get category():String
		{ return _categories[0]; }
		
		/** Return the list of categories */
		public function get categories():Vector.<String>
		{ return _categories; }
				
		public function addHandlers(... rest):void
		{
			_handlers = new Vector.<PropertyHandler>(rest.length,true);
			for (var idx:int = 0; idx < rest.length; idx++)
			{
				var handler:PropertyHandler = rest[idx]
				_handlers[idx] = handler;
				if (handler.customXMLStringHandler)
					_hasCustomExporterHandler = true;
				if (handler is NumberPropertyHandler)
					_numberPropertyHandler = handler as NumberPropertyHandler;
			}
		}
		
		public function findHandler(handlerClass:Class):PropertyHandler
		{
			for each (var prop:PropertyHandler in _handlers)
			{
				if (prop is handlerClass)
					return prop;
			}
			return null;
		}

		/** Helper function when setting the property */
		public function setHelper(currVal:*,newVal:*):*
		{
			for each (var handler:PropertyHandler in _handlers)
			{
				var checkRslt:* = handler.owningHandlerCheck(newVal);
				if (checkRslt !== undefined)
					return handler.setHelper(checkRslt);
			}
			
			Property.errorHandler(this,newVal);
			return currVal;	
		}
				
		/** Helper function when merging the property to compute actual attributes */
		public function concatInheritOnlyHelper(currVal:*,concatVal:*):*
		{ return (_inherited && currVal === undefined) || currVal == FormatValue.INHERIT ? concatVal : currVal; }

		public static function defaultConcatHelper(currVal:*,concatVal:*):*
		{ return currVal === undefined || currVal == FormatValue.INHERIT ? concatVal : currVal; }

		/** Helper function when merging the property to compute actual attributes */
		public function concatHelper(currVal:*,concatVal:*):*
		{
			if (_inherited)
				return currVal === undefined || currVal == FormatValue.INHERIT ? concatVal : currVal;
			if (currVal === undefined)
				return defaultValue;
			return currVal == FormatValue.INHERIT ? concatVal : currVal;
		}
		
		/** Helper function when comparing the property */
		public function equalHelper(v1:*,v2:*):Boolean
		{ return v1 == v2; }
		
		/** Convert the value of this property to a string appropriate for XML export */
		public function toXMLString(val:Object):String
		{
			if (_hasCustomExporterHandler)
			{
				for each (var prop:PropertyHandler in _handlers)
				{
					if (prop.customXMLStringHandler && prop.owningHandlerCheck(val) !== undefined)
						return prop.toXMLString(val);
				}				
			}
			return val.toString();
		}
		
		public function get maxPercentValue():Number
		{
			var handler:PercentPropertyHandler = findHandler(PercentPropertyHandler) as PercentPropertyHandler;
			return handler ? handler.maxValue : NaN;
		}
		public function get minPercentValue():Number
		{
			var handler:PercentPropertyHandler = findHandler(PercentPropertyHandler) as PercentPropertyHandler;
			return handler ? handler.minValue : NaN;
		}
		public function get minValue():Number
		{
			var numberHandler:NumberPropertyHandler = findHandler(NumberPropertyHandler) as NumberPropertyHandler;
			if (numberHandler)
				return numberHandler.minValue;
			var intHandler:IntPropertyHandler = findHandler(IntPropertyHandler) as IntPropertyHandler;
			return intHandler ? intHandler.minValue : NaN;
		}
		public function get maxValue():Number
		{
			var numberHandler:NumberPropertyHandler = findHandler(NumberPropertyHandler) as NumberPropertyHandler;
			if (numberHandler)
				return numberHandler.maxValue;
			var intHandler:IntPropertyHandler = findHandler(IntPropertyHandler) as IntPropertyHandler;
			return intHandler ? intHandler.maxValue : NaN;
		}
		
		public function computeActualPropertyValue(propertyValue:Object,percentInput:Number):Number
		{				
			var percent:Number = toNumberIfPercent(propertyValue);
			if (isNaN(percent))
				return Number(propertyValue);
			
			// its a percent - calculate and clamp
			var rslt:Number =  percentInput * (percent / 100);
			return _numberPropertyHandler ? _numberPropertyHandler.clampToRange(rslt) : rslt;
		}

		// /////////////////////////////////////////////
		// Following static functions are used by Format classes to 
		// perform functions that iterate over all the attributes.
		// They are driven by the attributes metadata object that contains
		// definitions for all the properties.
		// /////////////////////////////////////////////
			
		/** Helper function to initialize all property values from defaults. */
		static public function defaultsAllHelper(description:Object,current:Object):void
		{
			for each (var prop:Property in description)
				current[prop.name] = prop.defaultValue;
		}
		
		/** Helper function to compare two sets of properties. */
		static public function equalAllHelper(description:Object,p1:Object,p2:Object):Boolean
		{
			if (p1 == p2)
				return true;
			// these could be "equal" if all attributes of p1 or p2 are null
			if (p1 == null || p2 == null)
				return false;
			for each (var prop:Property in description)
			{
				var name:String = prop.name;
				if (!(prop.equalHelper(p1[name],p2[name])))
					return false;
			}
			return true;
		}

		static public function extractInCategory(formatClass:Class,description:Object,props:Object,category:String,legacy:Boolean = true):Object
		{
			var rslt:Object = null;
			for each (var prop:Property in description)
			{
				if (props[prop.name] == null)
					continue;
				
				if (legacy)
				{
					if (prop.category != category)
						continue;
				}
				else if (prop.categories.indexOf(category) == -1)
					continue;
				
				if (rslt == null)
					rslt = new formatClass();
				rslt[prop.name] = props[prop.name];
			}
			return rslt;
		}
		
		/** @private Copy an object */
		static public function shallowCopy(src:Object):Object
		{
			// make a shallow copy
			var rslt:Object = new Object()
			for (var val:Object in src)
				rslt[val] = src[val]; 
			return rslt;
		}
		
		/** @private Copy properties from src to result if a property of the same name exists in filter */
		static public function shallowCopyInFilter(src:Object,filter:Object):Object
		{
			// make a shallow copy
			var rslt:Object = new Object()
			for (var val:Object in src)
			{
				if (filter.hasOwnProperty(val))
					rslt[val] = src[val];
			}
			return rslt;
		}
		
		/** @private Copy properties from src to result if a property of the same name exists in filter */
		static public function shallowCopyNotInFilter(src:Object,filter:Object):Object
		{
			// make a shallow copy
			var rslt:Object = new Object()
			for (var val:Object in src)
			{
				if (!filter.hasOwnProperty(val))
					rslt[val] = src[val];
			}
			return rslt;
		}
		
		static private function compareStylesLoop(o1:Object,o2:Object,description:Object):Boolean
		{
			for (var val:String in o1)
			{
				var o1val:Object = o1[val];
				var o2val:Object = o2[val];
				if (o1val != o2val)
				{
					if (!(o1val is Array) || !(o2val is Array) || o1val.length != o2val.length || !description)
						return false;	// different
					var prop:ArrayProperty = description[val];
					if (!prop || !equalAllHelper(prop.memberType.tlf_internal::description,o1val,o2val))
						return false;
				}
			}
			return true;
		}
		/** @private */
		static tlf_internal const nullStyleObject:Object = new Object();
		/** @private */
		static public function equalStyles(o1:Object,o2:Object,description:Object):Boolean
		{
			if (o1 == null)
				o1 = nullStyleObject;
			if (o2 == null)
				o2 = nullStyleObject;
			// Use of prototype chains and bug https://bugzilla.mozilla.org/show_bug.cgi?id=447673 requires this two way compare
			return compareStylesLoop(o1,o2,description) && compareStylesLoop(o2,o1,description);
		}
		
		/** @private */
		static public function toNumberIfPercent(o:Object):Number
		{
			if (!(o is String))
				return NaN;
			var s:String = String(o);
			var len:int = s.length;
			
			return len != 0 && s.charAt(len-1) == "%" ? parseFloat(s) : NaN;
		}
		
		static private var prototypeFactory:Function = function():void
		{ }
		
		/** @private Create an object with specified prototype parent */
		static public function createObjectWithPrototype(parent:Object):Object
		{
			prototypeFactory.prototype = parent;
			return new prototypeFactory();
		}
	}
}
