package net.flashpunk
{
	import flash.display.BitmapData;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.utils.getQualifiedClassName;
	import flash.utils.getDefinitionByName;
	import net.flashpunk.masks.*;
	import net.flashpunk.graphics.*;
	
	/**
	 * Main game Entity class updated by World.
	 */
	public class Entity extends Tweener
	{
		/**
		 * If the Entity should render.
		 */
		public var visible:Boolean = true;
		
		/**
		 * If the Entity should respond to collision checks.
		 */
		public var collidable:Boolean = true;
		
		/**
		 * X position of the Entity in the World.
		 */
		public var x:Number = 0;
		
		/**
		 * Y position of the Entity in the World.
		 */
		public var y:Number = 0;
		
		/**
		 * Width of the Entity's hitbox.
		 */
		public var width:int;
		
		/**
		 * Height of the Entity's hitbox.
		 */
		public var height:int;
		
		/**
		 * X origin of the Entity's hitbox.
		 */
		public var originX:int;
		
		/**
		 * Y origin of the Entity's hitbox.
		 */
		public var originY:int;
		
		/**
		 * The BitmapData target to draw the Entity to. Leave as null to render to the current screen buffer (default).
		 */
		public var renderTarget:BitmapData;
		
		/**
		 * Constructor. Can be usd to place the Entity and assign a graphic and mask.
		 * @param	x			X position to place the Entity.
		 * @param	y			Y position to place the Entity.
		 * @param	graphic		Graphic to assign to the Entity.
		 * @param	mask		Mask to assign to the Entity.
		 */
		public function Entity(x:Number = 0, y:Number = 0, graphic:Graphic = null, mask:Mask = null) 
		{
			this.x = x;
			this.y = y;
			if (graphic) this.graphic = graphic;
			if (mask) this.mask = mask;
			HITBOX.assignTo(this);
			_class = Class(getDefinitionByName(getQualifiedClassName(this)));
		}
		
		/**
		 * Override this, called when the Entity is added to a World.
		 */
		public function added():void
		{
			
		}
		
		/**
		 * Override this, called when the Entity is removed from a World.
		 */
		public function removed():void
		{
			
		}
		
		/**
		 * Updates the Entity.
		 */
		override public function update():void 
		{
			
		}
		
		/**
		 * Renders the Entity. If you override this for special behaviour,
		 * remember to call super.render() to render the Entity's graphic.
		 */
		public function render():void 
		{
			if (_graphic && _graphic.visible)
			{
				if (_graphic.relative)
				{
					_point.x = x;
					_point.y = y;
				}
				else _point.x = _point.y = 0;
				_camera.x = FP.camera.x;
				_camera.y = FP.camera.y;
				_graphic.render(renderTarget ? renderTarget : FP.buffer, _point, _camera);
			}
		}
		
		/**
		 * Checks for a collision against an Entity type.
		 * @param	type		The Entity type to check for.
		 * @param	x			Virtual x position to place this Entity.
		 * @param	y			Virtual y position to place this Entity.
		 * @return	The first Entity collided with, or null if none were collided.
		 */
		public function collide(type:String, x:Number, y:Number):Entity
		{
			if (!_world) return null;
			
			var e:Entity = _world._typeFirst[type];
			if (!collidable || !e) return null;
			
			_x = this.x; _y = this.y;
			this.x = x; this.y = y;
			
			if (!_mask)
			{
				while (e)
				{
					if (x - originX + width > e.x - e.originX
					&& y - originY + height > e.y - e.originY
					&& x - originX < e.x - e.originX + e.width
					&& y - originY < e.y - e.originY + e.height
					&& e.collidable && e !== this)
					{
						if (!e._mask || e._mask.collide(HITBOX))
						{
							this.x = _x; this.y = _y;
							return e;
						}
					}
					e = e._typeNext;
				}
				this.x = _x; this.y = _y;
				return null;
			}
			
			while (e)
			{
				if (x - originX + width > e.x - e.originX
				&& y - originY + height > e.y - e.originY
				&& x - originX < e.x - e.originX + e.width
				&& y - originY < e.y - e.originY + e.height
				&& e.collidable && e !== this)
				{
					if (_mask.collide(e._mask ? e._mask : e.HITBOX))
					{
						this.x = _x; this.y = _y;
						return e;
					}
				}
				e = e._typeNext;
			}
			this.x = _x; this.y = _y;
			return null;
		}
		
		/**
		 * Checks for collision against multiple Entity types.
		 * @param	types		An Array or Vector of Entity types to check for.
		 * @param	x			Virtual x position to place this Entity.
		 * @param	y			Virtual y position to place this Entity.
		 * @return	The first Entity collided with, or null if none were collided.
		 */
		public function collideTypes(types:Object, x:Number, y:Number):Entity
		{
			if (!_world) return null;
			var e:Entity;
			for each (var type:String in types)
			{
				if ((e = collide(type, x, y))) return e;
			}
			return null;
		}
		
		/**
		 * Checks if this Entity collides with a specific Entity.
		 * @param	e		The Entity to collide against.
		 * @param	x		Virtual x position to place this Entity.
		 * @param	y		Virtual y position to place this Entity.
		 * @return	The Entity if they overlap, or null if they don't.
		 */
		public function collideWith(e:Entity, x:Number, y:Number):Entity
		{
			_x = this.x; _y = this.y;
			this.x = x; this.y = y;
			
			if (x - originX + width > e.x - e.originX
			&& y - originY + height > e.y - e.originY
			&& x - originX < e.x - e.originX + e.width
			&& y - originY < e.y - e.originY + e.height
			&& collidable && e.collidable)
			{
				if (!_mask)
				{
					if (!e._mask || e._mask.collide(HITBOX))
					{
						this.x = _x; this.y = _y;
						return e;
					}
					this.x = _x; this.y = _y;
					return null;
				}
				if (_mask.collide(e._mask ? e._mask : e.HITBOX))
				{
					this.x = _x; this.y = _y;
					return e;
				}
			}
			this.x = _x; this.y = _y;
			return null;
		}
		
		/**
		 * Checks if this Entity overlaps the specified rectangle.
		 * @param	x			Virtual x position to place this Entity.
		 * @param	y			Virtual y position to place this Entity.
		 * @param	rX			X position of the rectangle.
		 * @param	rY			Y position of the rectangle.
		 * @param	rWidth		Width of the rectangle.
		 * @param	rHeight		Height of the rectangle.
		 * @return	If they overlap.
		 */
		public function collideRect(x:Number, y:Number, rX:Number, rY:Number, rWidth:Number, rHeight:Number):Boolean
		{
			if (x - originX + width >= rX && y - originY + height >= rY
			&& x - originX <= rX + rWidth && y - originY <= rY + rHeight)
			{
				if (!_mask) return true;
				_x = this.x; _y = this.y;
				this.x = x; this.y = y;
				FP.entity.x = rX;
				FP.entity.y = rY;
				FP.entity.width = rWidth;
				FP.entity.height = rHeight;
				if (_mask.collide(FP.entity.HITBOX))
				{
					this.x = _x; this.y = _y;
					return true;
				}
				this.x = _x; this.y = _y;
				return false;
			}
			return false;
		}
		
		/**
		 * Checks if this Entity overlaps the specified position.
		 * @param	x			Virtual x position to place this Entity.
		 * @param	y			Virtual y position to place this Entity.
		 * @param	pX			X position.
		 * @param	pY			Y position.
		 * @return	If the Entity intersects with the position.
		 */
		public function collidePoint(x:Number, y:Number, pX:Number, pY:Number):Boolean
		{
			if (pX >= x - originX && pY >= y - originY
			&& pX < x - originX + width && pY < y - originY + height)
			{
				if (!_mask) return true;
				_x = this.x; _y = this.y;
				this.x = x; this.y = y;
				FP.entity.x = pX;
				FP.entity.y = pY;
				FP.entity.width = 1;
				FP.entity.height = 1;
				if (_mask.collide(FP.entity.HITBOX))
				{
					this.x = _x; this.y = _y;
					return true;
				}
				this.x = _x; this.y = _y;
				return false;
			}
			return false;
		}
		
		/**
		 * Populates an array with all collided Entities of a type.
		 * @param	type		The Entity type to check for.
		 * @param	x			Virtual x position to place this Entity.
		 * @param	y			Virtual y position to place this Entity.
		 * @param	array		The Array or Vector object to populate.
		 * @return	The array, populated with all collided Entities.
		 */
		public function collideInto(type:String, x:Number, y:Number, array:Object):void
		{
			if (!_world) return;
			
			var e:Entity = _world._typeFirst[type];
			if (!collidable || !e) return;
			
			_x = this.x; _y = this.y;
			this.x = x; this.y = y;
			var n:uint = array.length;
			
			if (!_mask)
			{
				while (e)
				{
					if (x - originX + width > e.x - e.originX
					&& y - originY + height > e.y - e.originY
					&& x - originX < e.x - e.originX + e.width
					&& y - originY < e.y - e.originY + e.height
					&& e.collidable && e !== this)
					{
						if (!e._mask || e._mask.collide(HITBOX)) array[n ++] = e;
					}
					e = e._typeNext;
				}
				this.x = _x; this.y = _y;
				return;
			}
			
			while (e)
			{
				if (x - originX + width > e.x - e.originX
				&& y - originY + height > e.y - e.originY
				&& x - originX < e.x - e.originX + e.width
				&& y - originY < e.y - e.originY + e.height
				&& e.collidable && e !== this)
				{
					if (_mask.collide(e._mask ? e._mask : e.HITBOX)) array[n ++] = e;
				}
				e = e._typeNext;
			}
			this.x = _x; this.y = _y;
			return;
		}
		
		/**
		 * Populates an array with all collided Entities of multiple types.
		 * @param	types		An array of Entity types to check for.
		 * @param	x			Virtual x position to place this Entity.
		 * @param	y			Virtual y position to place this Entity.
		 * @param	array		The Array or Vector object to populate.
		 * @return	The array, populated with all collided Entities.
		 */
		public function collideTypesInto(types:Object, x:Number, y:Number, array:Object):void
		{
			if (!_world) return;
			for each (var type:String in types) collideInto(type, x, y, array);
		}
		
		/**
		 * If the Entity collides with the camera rectangle.
		 */
		public function get onCamera():Boolean
		{
			return collideRect(x, y, FP.camera.x, FP.camera.y, FP.width, FP.height);
		}
		
		/**
		 * The World object this Entity has been added to.
		 */
		public function get world():World
		{
			return _world;
		}
		
		/**
		 * Half the Entity's width.
		 */
		public function get halfWidth():Number { return width / 2; }
		
		/**
		 * Half the Entity's height.
		 */
		public function get halfHeight():Number { return height / 2; }
		
		/**
		 * The center x position of the Entity's hitbox.
		 */
		public function get centerX():Number { return x - originX + width / 2; }
		
		/**
		 * The center y position of the Entity's hitbox.
		 */
		public function get centerY():Number { return y - originY + height / 2; }
		
		/**
		 * The leftmost position of the Entity's hitbox.
		 */
		public function get left():Number { return x - originX; }
		
		/**
		 * The rightmost position of the Entity's hitbox.
		 */
		public function get right():Number { return x - originX + width; }
		
		/**
		 * The topmost position of the Entity's hitbox.
		 */
		public function get top():Number { return y - originY; }
		
		/**
		 * The bottommost position of the Entity's hitbox.
		 */
		public function get bottom():Number { return y - originY + height; }
		
		/**
		 * The rendering layer of this Entity. Higher layers are rendered first.
		 */
		public function get layer():int { return _layer; }
		public function set layer(value:int):void
		{
			if (_layer == value) return;
			if (!_added)
			{
				_layer = value;
				return;
			}
			_world.removeRender(this);
			_layer = value;
			_world.addRender(this);
		}
		
		/**
		 * The collision type, used for collision checking.
		 */
		public function get type():String { return _type; }
		public function set type(value:String):void
		{
			if (_type == value) return;
			if (!_added)
			{
				_type = value;
				return;
			}
			if (_type) _world.removeType(this);
			_type = value;
			if (value) _world.addType(this);
		}
		
		/**
		 * An optional Mask component, used for specialized collision. If this is
		 * not assigned, collision checks will use the Entity's hitbox by default.
		 */
		public function get mask():Mask { return _mask; }
		public function set mask(value:Mask):void
		{
			if (_mask == value) return;
			if (_mask) _mask.assignTo(null);
			_mask = value;
			if (value) _mask.assignTo(this);
		}
		
		/**
		 * Graphical component to render to the screen.
		 */
		public function get graphic():Graphic { return _graphic; }
		public function set graphic(value:Graphic):void
		{
			if (_graphic == value) return;
			_graphic = value;
			if (value && value._assign != null) value._assign();
		}
		
		/**
		 * Adds the graphic to the Entity via a Graphiclist.
		 * @param	g		Graphic to add.
		 */
		public function addGraphic(g:Graphic):Graphic
		{
			if (graphic is Graphiclist) (graphic as Graphiclist).add(g);
			else
			{
				var list:Graphiclist = new Graphiclist;
				if (graphic) list.add(graphic);
				graphic = list;
			}
			return g;
		}
		
		/**
		 * Sets the Entity's hitbox properties.
		 * @param	width		Width of the hitbox.
		 * @param	height		Height of the hitbox.
		 * @param	originX		X origin of the hitbox.
		 * @param	originY		Y origin of the hitbox.
		 */
		public function setHitbox(width:int = 0, height:int = 0, originX:int = 0, originY:int = 0):void
		{
			this.width = width;
			this.height = height;
			this.originX = originX;
			this.originY = originY;
		}
		
		/**
		 * Sets the Entity's hitbox to match that of the provided object.
		 * @param	o		The object defining the hitbox (eg. an Image or Rectangle).
		 */
		public function setHitboxTo(o:Object):void
		{
			if (o is Image || o is Rectangle) setHitbox(o.width, o.height, -o.x, -o.y);
			else
			{
				if (o.hasOwnProperty("width")) width = o.width;
				if (o.hasOwnProperty("height")) height = o.height;
				if (o.hasOwnProperty("originX") && !(o is Graphic)) originX = o.originX;
				else if (o.hasOwnProperty("x")) originX = -o.x;
				if (o.hasOwnProperty("originY") && !(o is Graphic)) originX = o.originY;
				else if (o.hasOwnProperty("y")) originX = -o.y;
			}
		}
		
		/**
		 * Sets the origin of the Entity.
		 * @param	x		X origin.
		 * @param	y		Y origin.
		 */
		public function setOrigin(x:int = 0, y:int = 0):void
		{
			originX = x;
			originY = y;
		}
		
		/**
		 * Center's the Entity's origin (half width & height).
		 */
		public function centerOrigin():void
		{
			originX = width / 2;
			originY = height / 2;
		}
		
		/**
		 * Calculates the distance from another Entity.
		 * @param	e				The other Entity.
		 * @param	useHitboxes		If hitboxes should be used to determine the distance. If not, the Entities' x/y positions are used.
		 * @return	The distance.
		 */
		public function distanceFrom(e:Entity, useHitboxes:Boolean = false):Number
		{
			if (!useHitboxes) return Math.sqrt((x - e.x) * (x - e.x) + (y - e.y) * (y - e.y));
			return FP.distanceRects(x - originX, y - originY, width, height, e.x - e.originX, e.y - e.originY, e.width, e.height);
		}
		
		/**
		 * Calculates the distance from this Entity to the point.
		 * @param	px				X position.
		 * @param	py				Y position.
		 * @param	useHitboxes		If hitboxes should be used to determine the distance. If not, the Entities' x/y positions are used.
		 * @return	The distance.
		 */
		public function distanceToPoint(px:Number, py:Number, useHitbox:Boolean = false):Number
		{
			if (!useHitbox) return Math.sqrt((x - px) * (x - px) + (y - py) * (y - py));
			return FP.distanceRectPoint(px, py, x - originX, y - originY, width, height);
		}
		
		/**
		 * Calculates the distance from this Entity to the rectangle.
		 * @param	rx			X position of the rectangle.
		 * @param	ry			Y position of the rectangle.
		 * @param	rwidth		Width of the rectangle.
		 * @param	rheight		Height of the rectangle.
		 * @return	The distance.
		 */
		public function distanceToRect(rx:Number, ry:Number, rwidth:Number, rheight:Number):Number
		{
			return FP.distanceRects(rx, ry, rwidth, rheight, x - originX, y - originY, width, height);
		}
		
		/**
		 * Gets the class name as a string.
		 * @return	A string representing the class name.
		 */
		public function toString():String
		{
			var s:String = String(_class);
			return s.substring(7, s.length - 1);
		}
		
		/**
		 * Moves the Entity by the amount, retaining integer values for its x and y.
		 * @param	x			Horizontal offset.
		 * @param	y			Vertical offset.
		 * @param	solidType	An optional collision type to stop flush against upon collision.
		 * @param	sweep		If sweeping should be used (prevents fast-moving objects from going through solidType).
		 */
		public function moveBy(x:Number, y:Number, solidType:String = null, sweep:Boolean = false):void
		{
			_moveX += x;
			_moveY += y;
			x = Math.round(_moveX);
			y = Math.round(_moveY);
			_moveX -= x;
			_moveY -= y;
			if (solidType)
			{
				var sign:int, e:Entity;
				if (x != 0)
				{
					if (collidable && (sweep || collide(solidType, this.x + x, this.y)))
					{
						sign = x > 0 ? 1 : -1;
						while (x != 0)
						{
							if ((e = collide(solidType, this.x + sign, this.y)))
							{
								moveCollideX(e);
								break;
							}
							else
							{
								this.x += sign;
								x -= sign;
							}
						}
					}
					else this.x += x;
				}
				if (y != 0)
				{
					if (collidable && (sweep || collide(solidType, this.x, this.y + y)))
					{
						sign = y > 0 ? 1 : -1;
						while (y != 0)
						{
							if ((e = collide(solidType, this.x, this.y + sign)))
							{
								moveCollideY(e);
								break;
							}
							else
							{
								this.y += sign;
								y -= sign;
							}
						}
					}
					else this.y += y;
				}
			}
			else
			{
				this.x += x;
				this.y += y;
			}
		}
		
		/**
		 * Moves the Entity to the position, retaining integer values for its x and y.
		 * @param	x			X position.
		 * @param	y			Y position.
		 * @param	solidType	An optional collision type to stop flush against upon collision.
		 * @param	sweep		If sweeping should be used (prevents fast-moving objects from going through solidType).
		 */
		public function moveTo(x:Number, y:Number, solidType:String = null, sweep:Boolean = false):void
		{
			moveBy(x - this.x, y - this.y, solidType, sweep);
		}
		
		/**
		 * Moves towards the target position, retaining integer values for its x and y.
		 * @param	x			X target.
		 * @param	y			Y target.
		 * @param	amount		Amount to move.
		 * @param	solidType	An optional collision type to stop flush against upon collision.
		 * @param	sweep		If sweeping should be used (prevents fast-moving objects from going through solidType).
		 */
		public function moveTowards(x:Number, y:Number, amount:Number, solidType:String = null, sweep:Boolean = false):void
		{
			_point.x = x - this.x;
			_point.y = y - this.y;
			_point.normalize(amount);
			moveBy(_point.x, _point.y, solidType, sweep);
		}
		
		/**
		 * When you collide with an Entity on the x-axis with moveTo() or moveBy().
		 * @param	e		The Entity you collided with.
		 */
		public function moveCollideX(e:Entity):void
		{
			
		}
		
		/**
		 * When you collide with an Entity on the y-axis with moveTo() or moveBy().
		 * @param	e		The Entity you collided with.
		 */
		public function moveCollideY(e:Entity):void
		{
			
		}
		
		/**
		 * Clamps the Entity's hitbox on the x-axis.
		 * @param	left		Left bounds.
		 * @param	right		Right bounds.
		 * @param	padding		Optional padding on the clamp.
		 */
		public function clampHorizontal(left:Number, right:Number, padding:Number = 0):void
		{
			if (x - originX < left + padding) x = left + originX + padding;
			if (x - originX + width > right - padding) x = right - width + originX - padding;
		}
		
		/**
		 * Clamps the Entity's hitbox on the y axis.
		 * @param	top			Min bounds.
		 * @param	bottom		Max bounds.
		 * @param	padding		Optional padding on the clamp.
		 */
		public function clampVertical(top:Number, bottom:Number, padding:Number = 0):void
		{
			if (y - originY < top + padding) y = top + originY + padding;
			if (y - originY + height > bottom - padding) y = bottom - height + originY - padding;
		}
		
		// Entity information.
		/** @private */ internal var _class:Class;
		/** @private */ internal var _world:World;
		/** @private */ internal var _added:Boolean;
		/** @private */ internal var _type:String = "";
		/** @private */ internal var _layer:int;
		/** @private */ internal var _updatePrev:Entity;
		/** @private */ internal var _updateNext:Entity;
		/** @private */ internal var _renderPrev:Entity;
		/** @private */ internal var _renderNext:Entity;
		/** @private */ internal var _typePrev:Entity;
		/** @private */ internal var _typeNext:Entity;
		/** @private */ internal var _recycleNext:Entity;
		
		// Collision information.
		/** @private */ private const HITBOX:Mask = new Mask;
		/** @private */ private var _mask:Mask;
		/** @private */ private var _x:Number;
		/** @private */ private var _y:Number;
		/** @private */ private var _moveX:Number = 0;
		/** @private */ private var _moveY:Number = 0;
		
		// Rendering information.
		/** @private */ internal var _graphic:Graphic;
		/** @private */ private var _point:Point = FP.point;
		/** @private */ private var _camera:Point = FP.point2;
	}
}