//= require <mootools-core-1.3-full-nocompat>
//= require <uiframework/EventDispatcher>
//= require <uiframework/MethodQueueElement>
//= require <uiframework/UIDUtil>
//= require <uiframework/UICEvent>
/**
 * The base class for visual elements in the DOM
 * Provides event dispatching capabilities with bubbling
 * and capture phases, sizing, positioning and child objects
 */
var UIComponent = function(){
    
	this.Extends = EventDispatcher;
	
	/**
     * Constructor
     * @param element A string or an Element object representing the DOM element to use.
     * @param properties (optional) An Object containing the prperties to set on the newly created element.
     */
	this.initialize = function(element, properties)
	{
		this.parent();
		// Declare instance properties.
		this.parentComponent = null;
		this.invalidatePropertiesFlag = false;
		this.invalidateSizeFlag = false;
		this.invalidateDisplayListFlag = false;
		this.updateCompletePending = false;
		this.methodQueue = [];
		this.timeoutId = 0;
		this.name = '';
		//--------------------------
		this.numChildren = 0;
		this._displayList = [];
		this._x = 0;
		this._y = 0,
		this._width = null;
		this._height = null;
		this._explicitWidth = Number.NaN;
		this._explicitHeight = Number.NaN;
		this._minWidth = Number.NaN;
		this._minHeight = Number.NaN;
		this._measuredWidth = 0;
		this._measuredHeight = 0;
		this._enabled = true;
		this.nestLevel = 0;
		this.widthChanged = false;
		this.heightChanged = false;
		this.coordChanged = false;
		this.initialized = false;
		this._visible = null;
		//----------------------------
		this.stateChanged = false;
		this.currentState = null;
		this.newStatePrevented = false;
		this.newState = null;
		//----------------------------
		if (element)
		{
			this.element = $(element);
			if (!this.element)
				this.element = new Element(element, properties);
			this.initializeElement();
			this.set(properties);
			this.name = this.element.id ? this.element.id : this.element.get('tag')+UIDUtil.createUID();
		}
	};

	/**
     * Checks the specified element against the list
     * of elements already in use and if the element is not
     * found, it is initialized into the UIComponent.
     */
	this.initializeElement = function()
	{
		var defaultStyles = {
			position:"absolute",
			visibility:"hidden"
		};
		this.element.setStyles(defaultStyles);
		// Copy all necessary methods of the Element object to
		// This and bind the functions to the element itself
		var e = this.element;
		for (var key in e)
		{
			var type = null;
			try
			{
				type = typeof e[key];
			}
			catch(e){}
			if (type == "function" && !/(event)/i.test(key) && !this[key])
			{
				try
				{
					this[key] = e[key].bind(e);
				}
				catch(e){}
			}
		}
	};

	this.commitProperties = function()
	{
		if (this.stateChanged)
		{
			this.commitNewState();
			if (!this.newStatePrevented)
				this.currentStateCommitted();
			this.newState = null;
			this.stateChanged = false;
		}
	};

	/**
     * Stores the default size of the component.
     * This method is called automatically by the layoutManager
     * when a call to invalidateSize() is made.
     */
	this.measure = function()
	{
		var size = this.getSize();
		this._measuredWidth = size.x;
		this._measuredHeight =size.y;
	};

	this.createChildren = function()
	{
	
	};

	this.childrenCreated = function()
	{
		this.invalidateProperties();
		this.invalidateSize();
		this.invalidateDisplayList();
	};

	this.initializationComplete = function()
	{
		if (this._visible != null)
			this.visible(this._visible);
		else
			this.visible(true);
		if (this.hasEventListener("creationComplete"))
			this.dispatchEvent(new UICEvent("creationComplete", false, false, null));
	};

	/**
     * Updates the layout of this object.  May include placement
     * of child elements, changes to styles, colors or backgrounds.
     * The bulk of the visual change to the element occurs here.
     *
     * @param width Number used to specify the width of this component.
     * @param height Number used to specify the height of this component.
     */
	this.updateDisplayList = function(width, height)
	{
		if (this.widthChanged)
		{
			this.setStyle("width", isNaN(width) ? 0 : width);
			this.widthChanged = false;
		}

		if (this.heightChanged)
		{
			this.setStyle("height", isNaN(height) ? 0 : height);
			this.heightChanged = false;
		}

		if (this.coordChanged)
		{
			this.setStyles(
			{
				top:this._y,
				left:this._x
			});
			this.coordChanged = false;
		}
	};

	/**
     * Adds an element as a child to this object's element
     * if the element was previously parented, it is removed
     * from that parent.
     *
     * @param {UIComponent} child The UIComponent instance to add as a child.
     * @param {String} where can be top or bottom - matches the arguments of the MooTools 'grab' operation
     * @return UIComponent - The UIComponent just added.
     */
	this.addChild = function(child /* UIComponent */, where /* String */)
	{
		if (!child)
			throw new Error("Child must not be null.");
		var formerParent = child.parentComponent;
		if (formerParent)
			formerParent.removeChild(child);
		// Determine if we are wrapped in a link
		var toGrab = child.getParent('a');
		if (!toGrab)
			toGrab = child.element;
		this.addingChild(toGrab, child, where);
		child.nestLevel = this.nestLevel + 1;
		if (!child.initialized)
		{
			child.createChildren();
			child.childrenCreated();
			child.initializationComplete();
		}
		else if (!formerParent)
		{
			child.invalidateProperties();
			child.invalidateSize();
			child.invalidateDisplayList();
		}
		return child;
	};

	/**
     * @private
     */
	this.addingChild = function(element /* Element */, child /*UIComponent */, where /*String */)
	{
		if (!this.contains(child))
		{
			this.numChildren ++;
			this._displayList[this.numChildren-1] = child;
			this.grab(element, where);
			child.parentComponent = this;
			child.dispatchEvent(new UICEvent("added", false, false, null));
		}
	};
    
	/**
     * Removes the specified child element from
     * the DOM if it is a child of this object's element.
     *
     * @param child the UIComponent instance to remove.
     */
	this.removeChild = function(child)
	{
		var toDispose = child.getParent('a');
		// Determine if we are wrapped in a link
		if (!toDispose)
			toDispose = child.element;
		this.removingChild(toDispose, child);
		child.parentComponent = null;
		return child;
	};

	/**
     * @private
     */
	this.removingChild = function(element /* Element */, child /* UIComponent */)
	{
		var i  = this._displayList.length;
		while(i--)
		{
			if (this._displayList[i] == child)
			{
				this._displayList.splice(i, 1);
				this.numChildren --;
				element.dispose();
				if (this.hasEventListener("removed"))
					child.dispatchEvent(new UICEvent("removed", false, false, child));
				return;
			}
		}
		throw new Error("Element does not contain the child object.");
	};

	/**
     * Determines if a UIComponent is in this child list,
     * or is a descendant of an child in this child list.
     */
	this.contains = function(child /* UIComponent */)
	{
		var i  = this._displayList.length;
		while(i--)
		{
			if (this._displayList[i] == child || this._displayList[i].contains(child))
				return true;
		}
		return false;
	};

	/**
	 * Returns the child at the specified index
	 *
	 * @param index {int} The index of the child to retrieve.
	 */
	this.getChildAt = function(index)
	{
		return this._displayList[index];
	}

	/**
     * Calls the specified function at a later time.
     *
     * @param method The Function to call later.
     * @param arguments The arguments to be passed to the function.
     */
	this.callLater = function(method, arguments)
	{
		this.methodQueue.push(new MethodQueueElement(method, arguments));
		if (!this.timeoutId)
			this.timeoutId = this.callLaterDispatcher.bind(this).delay(10);
	};

	/**
     * Call queued methods at the specified interval.
     */
	this.callLaterDispatcher = function()
	{
		var queue = this.methodQueue;
		this.methodQueue = [];
		var n = queue.length;
		for (var i = 0; i < n; i++)
		{
			var mqe = queue[i]; /*MethodQueueElement*/
			mqe.method.apply(null, mqe.arguments ? mqe.arguments : null);
		}
		// Sometimes the methods being called will use callLater.
		// If this is the case, this.methodQueue will have length
		// and a new timer needs to be created
		if (this.methodQueue.length)
			this.timeoutId = this.callLaterDispatcher.bind(this).delay(10);
		else
			this.timeoutId = null;
	};

	/**
	 * Gets border widths for each side
	 * and returns an object containing those values.
	 *
	 * @return {Object} containing top, right, bottom and left border widths
	 */
	this.getBorderMetrics = function()
	{
		var top = this.getStyle("border-top-width").toFloat();
		var right = this.getStyle("border-right-width").toFloat();
		var bottom = this.getStyle("border-bottom-width").toFloat();
		var left = this.getStyle("border-left-width").toFloat();

		return {
			top:top,
			right:right,
			bottom:bottom,
			left:left
		};
	};

	/**
     * Sets the size of the Object.
     *
     * @param w The Number in pixels to set as the width of the object.
     * @param h The Number in pixels to set as the height of the object.
     */
	this.setActualSize = function(w, h)
	{
		this.width(w);
		this.height(h);
	};

	/**
     * Moved the phisical location of this object's Element
     * relative to it's parent.
     */
	this.move = function(x, y)
	{
		if (this._x != x)
		{
			this._x = x;
			this.coordChanged = true;
		}
		if (this._y != y)
		{
			this._y = y;
			this.coordChanged = true;
		}

		if (this.coordChanged)
			this.invalidateDisplayList();
	};

	/**
     * The default height of the component in pixels.
     * The measuredHeight may be different than the
     * actual height.
     *
     * @param value Number representing the new measuredHeight
     * of the component - usually set automatically by the measure() method
     * @return Number representing the current measured height
     */
	this.measuredHeight = function(value)
	{
		if (typeof value == "number")
			this._measuredHeight = value;
		return this._measuredHeight;
	};

	/**
     * The default width of the component in pixels.
     * The measuredHeight may be different than the
     * actual height.
     *
     * @param value Number representing the new measuredWidth
     * of the component - usually set automatically  by the measure() method
     * @return Number representing the current measured width
     */
	this.measuredWidth = function(value)
	{
		if (typeof value == "number")
			this._measuredWidth = value;
		return this._measuredWidth;
	};

	/**
     * -----------------------------------------------------------------------
     * Alias methods
     * -----------------------------------------------------------------------
     */

	this.minWidth = function(value /* Number */)
	{
		if (typeof value == 'number' && this._minWidth != value)
		{
			this._minWidth = value;
			if (this.parentComponent)
			{
				var p = this.parentComponent;
				p.invalidateProperties();
				p.invalidateDisplayList();
			}
			this.invalidateDisplayList();
			this.invalidateSize();
		}
		return this._minWidth;
	}

	this.minHeight = function(value /* Number */)
	{
		if (typeof value == 'number' && this._minHeight != value)
		{
			this._minHeight = value;
			if (this.parentComponent)
			{
				var p = this.parentComponent;
				p.invalidateProperties();
				p.invalidateDisplayList();
			}
			this.invalidateDisplayList();
			this.invalidateSize();
		}
		return this._minHeight;
	}

	/**
     * Gets / sets the height of the element
     */
	this.width = function(value)
	{
		if (typeof value == "number" && this._width != value)
		{
			this.widthChanged = true;

			this.invalidateProperties();
			this.invalidateDisplayList();

			if (this.parentComponent)
			{
				var p = this.parentComponent;
				p.invalidateProperties();
				p.invalidateDisplayList();
			}
			this._width = value < 0 ? 0 : value;
			this._explicitWidth = this._width;
			if (this.hasEventListener("widthChanged"))
				this.dispatchEvent(new UICEvent("widthChanged", false, false, null));
		}
		return this._width != null ? this._width : this.getSize().x + this.getStyle('margin-left').toPixels() + this.getStyle('margin-right').toPixels();
	};

	this.height = function(value)
	{
		if (typeof value == "number" && this._height != value)
		{
			this.heightChanged = true;

			this.invalidateProperties();
			this.invalidateDisplayList();

			if (this.parentComponent)
			{
				var p = this.parentComponent;
				p.invalidateProperties();
				p.invalidateDisplayList();
			}
			this._height = value < 0 ? 0 : value;
			this._explicitHeight = this._height;
			if (this.hasEventListener("heightChanged"))
				this.dispatchEvent(new UICEvent("heightChanged", false, false, null));
		}
		return this._height != null ? this._height : this.getSize().y + this.getStyle('margin-top').toPixels() + this.getStyle('margin-bottom').toPixels();
	};

	this.visible = function(value)
	{
		if (!this.element)
			return false;
		
		if (typeof value == "boolean" && this._visible != value)
		{
			this._visible = value;
			var stringValue = value ? "visible":"hidden";
			this.setStyle("visibility", stringValue);
		}
		return this._visible;
	};
	
	this.enabled = function(value)
	{
		if (typeof value == "boolean" && this._enabled != value)
		{
			this._enabled = value;
			this.invalidateProperties();
			this.invalidateDisplayList();
		}
		return this._enabled;
	};

	this.state = function(value)
	{
		if (arguments.length && this.currentState != value)
		{
			this.stateChanged = true;
			this.newState = value;

			this.invalidateProperties();
			this.invalidateDisplayList();
		}
		return this.newState;
	};

	this.commitNewState = function()
	{
		if (this.hasEventListener("stateChanging"))
		{
			this.dispatchEvent(new UICEvent("stateChanging", false, false, {
				oldState:this.currentState,
				newState:this.newState
			}));
		}
		this.stateChanged = false;
	};

	this.preventNewState = function()
	{
		this.newStatePrevented = true;
	};

	this.currentStateCommitted = function()
	{
		var oldState = this.currentState;
		this.currentState = this.newState;
		if (this.hasEventListener("stateChanged"))
		{
			this.dispatchEvent(new UICEvent("stateChanged", false, false, {
				oldState:oldState,
				newState:this.currentState
			}));
		}
	};

	/**
     * -----------------------------------------------------------------------
     * Element overrides
     * -----------------------------------------------------------------------
     */
	this.set = function()
	{
		this.element.set.apply(this.element, arguments);
	
		this.invalidateProperties();
		this.invalidateDisplayList();
		this.invalidateSize();
		if (this.parentComponent)
			this.parentComponent.invalidateDisplayList();
	};

	this.setStyle = function()
	{
		this.element.setStyle.apply(this.element, arguments);

		this.invalidateProperties();
		this.invalidateDisplayList();
		this.invalidateSize();
		if (this.parentComponent)
			this.parentComponent.invalidateDisplayList();
	};

	/**
     * -----------------------------------------------------------------------
     * Layout methods
     * -----------------------------------------------------------------------
     */

	this.invalidateProperties = function()
	{
		if (!this.invalidatePropertiesFlag)
		{
			this.invalidatePropertiesFlag = true;
			LayoutManager.invalidateProperties(this);
		}
	};

	this.invalidateSize = function()
	{
		if (!this.invalidateSizeFlag)
		{
			this.invalidateSizeFlag = true;
			LayoutManager.invalidateSize(this);
		}
	};

	this.invalidateDisplayList = function()
	{
		if (!this.invalidateDisplayListFlag)
		{
			this.invalidateDisplayListFlag = true;
			LayoutManager.invalidateDisplayList(this);
		}
	};

	/**
     * Validates the properties of the object
     * by calling validateProperties()
     */
	this.validateProperties = function()
	{
		if (this.invalidatePropertiesFlag)
		{
			this.commitProperties();
			this.invalidatePropertiesFlag = false;
		}
	};

	/**
     * Validates the size of the object by calling
     * measure()
     */
	this.validateSize = function()
	{
		if (this.invalidateSizeFlag)
		{
			// No reason to re-measure if the width or height are known.
			if (isNaN(this._explicitWidth) || isNaN(this._explicitHeight))
				this.measure();
			this.invalidateSizeFlag = false;
		}
	};

	this.validateDisplayList = function()
	{
		if (this.invalidateDisplayListFlag)
		{
			this.updateDisplayList(this.width(), this.height());
			this.invalidateDisplayListFlag = false;
		}
	};

	this.validateNow = function()
	{
		LayoutManager.validateClient(this);
	}

	/**
     * Destroys the UIComponent instance
     */
	this.destruct = function()
	{
	
	};
};
UIComponent = new Class(new UIComponent());
