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;
    }
}