package flare.display
{
    import flash.display.DisplayObject;
    import flash.display.Sprite;
    import flash.display.Stage;
    import flash.events.Event;
    import flash.geom.Point;
    import flash.geom.Rectangle;
    
    /**
     * A Sprite that redraws itself as needed when marked as "dirty".
     * This allows multiple changes to be made to the sprite between frames
     * without triggering a potentially costly redraw for each property update.
     * Instead, the <code>dirty()</code> method should be called whenever a
     * change is made to the Sprite that would normally require a redraw. This
     * class will ensure that the Sprite is redrawn only once before the next
     * frame is rendered.
     * 
     * <p>Subclasses should place drawing code within the <code>render()</code>
     * method. For all properties used by the <code>render</code> method to help
     * draw this sprite, the corresponding "setter" method should call the
     * <code>dirty()</code> method to mark the sprite as dirty and thereby
     * trigger a redraw for the next frame.</p>
     * 
     * <p>Internally, the DirtySprite class maintains a static list of all
     * "dirty" sprites, and redraws each sprite in the list when a
     * <code>Event.RENDER</code> event is issued. Typically, this process is
     * performed automatically. In a few cases, erratic behavior has been
     * observed due to a Flash Player bug that results in <code>RENDER</code>
     * events not being properly issued. As a fallback, the static
     * <code>renderDirty()</code> method can be invoked to manually force
     * each dirty sprite to be redrawn.</p>
     */
    public class DirtySprite extends Sprite
    {
        private static var __stage:Stage;
        private static var __installed:Boolean = false;
        private static var __dirtyList:Array = [];
        
        /**
         * Installs the frame render listener on the stage.
         */    
        private static function install(stage:Stage):void
        {
            __stage = stage;
            __stage.addEventListener(Event.RENDER, renderDirty);
            __installed = true;
        }
        
        /**
         * Frame render callback that renders all sprites on the dirty list.
         * Typically, this method is automatically triggered by stage
         * RENDER events. It can also be manually invoked to redraw all
         * dirty DirtySprites.
         * @param evt the event that triggered the rendering (can be null)
         */
        public static function renderDirty(evt:Event=null):void
        {
            while (__dirtyList.length > 0) {
                var ds:DirtySprite = DirtySprite(__dirtyList.pop());
                var db:Boolean = (ds._dirty == DIRTY);
                ds._dirty = CLEAN;
                if (db) ds.render();
            }

            // We need to remove and then re-add the listeners
            // to work around Flash Player bugs (#139381?). Ugh.
            // TODO: it seems this is not a complete solution, as in
            // rare cases RENDER events are still omitted.
            if (__stage != null) {
                __stage.removeEventListener(Event.RENDER, renderDirty);
                __installed = false;
            }
        }
        
        // --------------------------------------------------------------------
        
        private static const CLEAN:int = 0; // no changes
        private static const DIRTY:int = 1; // re-rendering needed
        private static const VISIT:int = 2; // was re-rendered, but on list
        
        /** @private */
        protected var _dirty:int = DIRTY; // dirty at birth
        
        /**
         * Creates a new DirtySprite. Registers this Sprite to receive
         * added-to-stage events.
         */
        public function DirtySprite() {
            this.addEventListener(Event.ADDED_TO_STAGE, onAddToStage,
                                  false, 0, true); // use weak reference
            __dirtyList.push(this);
        }
        
        /**
         * Makes sure that "dirtying" changes made to this Sprite
         * while it is off the display list still result in a
         * re-rendering if the Sprite is ever added to the list.
         */
        private function onAddToStage(evt:Event):void
        {
            if (_dirty == DIRTY) {
                if (!__installed) install(stage);    
                stage.invalidate();
            }
        }

        /**
         * Marks this sprite as "dirty" and in need of re-rendering.
         * The next time that (a) a new frame is rendered, and
         * (b) this Sprite is on the display list, the render method
         * will automatically be called.
         */
        public final function dirty():void
        {
            if (_dirty == DIRTY) return;
            
            __dirtyList.push(this);
            if (stage) {    
                if (!__installed) install(stage);
                stage.invalidate();
            }
            _dirty = DIRTY;
        }
        
        /**
         * If dirty, this sprite is re-rendered before returning the width.
         */
        public override function get width():Number
        {
            if (_dirty == DIRTY) { _dirty = VISIT; render(); }
            return super.width;
        }
        
        /**
         * If dirty, this sprite is re-rendered before returning the height.
         */
        public override function get height():Number
        {
            if (_dirty == DIRTY) { _dirty = VISIT; render(); }
            return super.height;
        }
        
        /**
         * If dirty, this sprite is re-rendered before returning the rect.
         */
        public override function getRect(targetCoordinateSpace:DisplayObject):Rectangle
        {
            if (_dirty == DIRTY) { _dirty = VISIT; render(); }
            return super.getRect(targetCoordinateSpace);
        }
        
        /**
         * If dirty, this sprite is re-rendered returning the bounds.
         */
        public override function getBounds(targetCoordinateSpace:DisplayObject):Rectangle
        {
            if (_dirty == DIRTY) { _dirty = VISIT; render(); }
            return super.getBounds(targetCoordinateSpace);
        }
        
        /**
         * If dirty, either sprite is re-rendered before hit-testing.
         */
        public override function hitTestObject(obj:DisplayObject):Boolean
        {
            if (_dirty == DIRTY) { _dirty = VISIT; render(); }
            var ds:DirtySprite = obj as DirtySprite;
            if (ds && ds._dirty == DIRTY) { ds._dirty = VISIT; ds.render(); }
            return super.hitTestObject(obj);
        }
        
        /**
         * If dirty, this sprite is re-rendered before hit-testing.
         */
        public override function hitTestPoint(x:Number, y:Number, shapeFlag:Boolean=false):Boolean
        {
            if (_dirty == DIRTY) { _dirty = VISIT; render(); }
            return super.hitTestPoint(x, y, shapeFlag);
        }
        
        /**
         * Draw this sprite's graphical content. Subclasses should
         * override this method with custom drawing code.
         */
        public function render():void
        {
            // for sub-classes to override...
        }
        
        /** @inheritDoc */
        public override function toString():String
        {
            var s:String = super.toString();
            return name==null ? s : s + " \""+name+"\"";
        }
        
        
        // -- Polar Coordinates -----------------------------------------------
        
        /** A constant for the point (0,0). */
        public static const ZERO:Point = new Point(0,0);
        
        /** The radius value of this sprite's position in polar co-ordinates.
         *  Polar coordinate values are assume a circle center given by the
         *  <code>origin</code> property. */
        protected var _radius:Number;
        
        /** The angle value of this sprite's position in polar co-ordinates.
         *  Polar coordinate values are assume a circle center given by the
         *  <code>origin</code> property. */
        protected var _angle:Number;
        
        /** The origin point for polar coordinates. */
        protected var _origin:Point = ZERO;
        
        /** @inheritDoc */
        public override function set x(v:Number):void {
            super.x = v; _radius = NaN; _angle = NaN;
        }
        /** @inheritDoc */
        public override function set y(v:Number):void {
            super.y = v; _radius = NaN; _angle = NaN;
        }
        
        /** The radius value of this sprite's position in polar co-ordinates.
         *  Polar coordinate values are assume a circle center given by the
         *  <code>origin</code> property. */
        public function get radius():Number {
            if (isNaN(_radius)) {
                var cx:Number = x - _origin.x;
                var cy:Number = y - _origin.y;
                _radius = Math.sqrt(cx*cx + cy*cy);
            }
            return _radius;
        }
        public function set radius(r:Number):void {
            var a:Number = angle;
            super.x =  r * Math.cos(a) + _origin.x;
            super.y = -r * Math.sin(a) + _origin.y;
            _radius = r;
        }
        
        /** The angle value of this sprite's position in polar co-ordinates.
         *  Polar coordinate values are assume a circle center given by the
         *  <code>origin</code> property. */
        public function get angle():Number {
            if (isNaN(_angle)) {
                _angle = Math.atan2(-(y-_origin.y),(x-_origin.x));
            }
            return _angle;
        }
        public function set angle(a:Number):void {
            var r:Number = radius;
            super.x =  r * Math.cos(a) + _origin.x;
            super.y = -r * Math.sin(a) + _origin.y;
            _angle = a;
        }
        
        /** The origin point for polar coordinates. */
        public function get origin():Point { return _origin; }
        public function set origin(p:Point):void {
            if (p.x != _origin.x || p.y != _origin.y) {
                _radius = NaN; _angle = NaN;
            }
            _origin = p;
        }
        
    } // end of class DirtySprite
}