    package flare.physics
    {
        import flash.geom.Rectangle;

        /**
         * A physical simulation involving particles, springs, and forces.
         * Useful for simulating a range of physical effects or layouts.
         */
        public class Simulation
        {
            private var _particles:Array = new Array();
            private var _springs:Array = new Array();
            private var _forces:Array = new Array();
            private var _bounds:Rectangle = null;

            /** The default gravity force for this simulation. */
            public function get gravityForce():GravityForce {
                return _forces[0] as GravityForce;
            }

            /** The default n-body force for this simulation. */
            public function get nbodyForce():NBodyForce {
                return _forces[1] as NBodyForce;
            }

            /** The default drag force for this simulation. */
            public function get dragForce():DragForce {
                return _forces[2] as DragForce;
            }

            /** The default spring force for this simulation. */
            public function get springForce():SpringForce {
                return _forces[3] as SpringForce;
            }

            /** Sets a bounding box for particles in this simulation.
             *  Null (the default) indicates no boundaries. */
            public function get bounds():Rectangle { return _bounds; }
            public function set bounds(b:Rectangle):void {
                if (_bounds == b) return;
                if (b == null) { _bounds = null; return; }
                if (_bounds == null) { _bounds = new Rectangle(); }
                // ensure x is left-most and y is top-most
                _bounds.x = b.x + (b.width < 0 ? b.width : 0);
                _bounds.width = (b.width < 0 ? -1 : 1) * b.width;
                _bounds.y = b.y + (b.width < 0 ? b.height : 0);
                _bounds.height = (b.height < 0 ? -1 : 1) * b.height;
            }

            /**
             * Creates a new physics simulation.
             * @param gx the gravitational acceleration along the x dimension
             * @param gy the gravitational acceleration along the y dimension
             * @param drag the default drag (viscosity) co-efficient
             * @param attraction the gravitational attraction (or repulsion, for
             *  negative values) between particles.
             */
            public function Simulation(gx:Number=0, gy:Number=0,
                drag:Number=0.1, attraction:Number=-5)
            {
                _forces.push(new GravityForce(gx, gy));
                _forces.push(new NBodyForce(attraction));
                _forces.push(new DragForce(drag));
                _forces.push(new SpringForce());
            }

            // -- Init Simulation -------------------------------------------------

            /**
             * Adds a custom force to the force simulation.
             * @param force the force to add
             */
            public function addForce(force:IForce):void
            {
                _forces.push(force);
            }

            /**
             * Returns the force at the given index.
             * @param idx the index of the force to look up
             * @return the force at the specified index
             */
            public function getForceAt(idx:int):IForce
            {
                return _forces[idx];
            }

            /**
             * Adds a new particle to the simulation.
             * @param mass the mass (charge) of the particle
             * @param x the particle's starting x position
             * @param y the particle's starting y position
             * @return the added particle
             */
            public function addParticle(mass:Number, x:Number, y:Number):Particle
            {
                var p:Particle = getParticle(mass, x, y);
                _particles.push(p);
                return p;
            }

            /**
             * Removes a particle from the simulation. Any springs attached to
             * the particle will also be removed.
             * @param idx the index of the particle in the particle list
             * @return true if removed, false otherwise.
             */
            public function removeParticle(idx:uint):Boolean
            {
                var p:Particle = _particles[idx];
                if (p == null) return false;

                // remove springs
                for (var i:uint = _springs.length; --i >= 0; ) {
                    var s:Spring = _springs[i];
                    if (s.p1 == p || s.p2 == p)
                        removeSpring(i);
                }
                // remove from particles
                reclaimParticle(p);
                _particles.splice(idx, 1);
                return true;
            }

            /**
             * Adds a spring to the simulation
             * @param p1 the first particle attached to the spring
             * @param p2 the second particle attached to the spring
             * @param restLength the rest length of the spring
             * @param tension the tension of the spring
             * @param damping the damping (friction) co-efficient of the spring
             * @return the added spring
             */
            public function addSpring(p1:Particle, p2:Particle, restLength:Number,
                                      tension:Number, damping:Number):Spring
            {
                var s:Spring = getSpring(p1, p2, restLength, tension, damping);
                p1.degree++;
                p2.degree++;
                _springs.push(s);
                return s;
            }

            /**
             * Removes a spring from the simulation.
             * @param idx the index of the spring in the spring list
             * @return true if removed, false otherwise
             */
            public function removeSpring(idx:uint):Boolean
            {
                if (idx >= _springs.length) return false;
                var s:Spring = _springs[idx];
                s.p1.degree--;
                s.p2.degree--;
                reclaimSpring(s);
                _springs.splice(idx, 1);
                return true;
            }

            /**
             * Returns the particle list. This is the same array instance backing
             * the simulation, so edit the array with caution.
             * @return the particle list
             */
            public function get particles():Array {
                return _particles;
            }

            /**
             * Returns the spring list. This is the same array instance backing
             * the simulation, so edit the array with caution.
             * @return the spring list
             */
            public function get springs():Array {
                return _springs;
            }

            // -- Run Simulation --------------------------------------------------

            /**
             * Advance the simulation for the specified time interval.
             * @param dt the time interval to step the simulation (default 1)
             */
            public function tick(dt:Number=1):void
            {
                var p:Particle, s:Spring, i:uint, ax:Number, ay:Number;
                var dt1:Number = dt/2, dt2:Number = dt*dt/2;

                // remove springs connected to dead particles
                for (i=_springs.length; --i>=0;) {
                    s = _springs[i];
                    if (s.die || s.p1.die || s.p2.die) {
                        s.p1.degree--;
                        s.p2.degree--;
                        reclaimSpring(s);
                        _springs.splice(i, 1);
                    }
                }

                // update particles using Verlet integration
                for (i=_particles.length; --i>=0;) {
                    p = _particles[i];
                    p.age += dt;
                    if (p.die) { // remove dead particles
                        reclaimParticle(p);
                        _particles.splice(i, 1);
                    } else if (p.fixed) {
                        p.vx = p.vy = 0;
                    } else {
                        ax = p.fx / p.mass; ay = p.fy / p.mass;
                        p.x  += p.vx*dt + ax*dt2;
                        p.y  += p.vy*dt + ay*dt2;
                        p._vx = p.vx + ax*dt1;
                        p._vy = p.vy + ay*dt1;
                    }
                }
                // evaluate the forces
                eval();
                // update particle velocities
                for (i=_particles.length; --i>=0;) {
                    p = _particles[i];
                    if (!p.fixed) {
                        ax = dt1 / p.mass;
                        p.vx = p._vx + p.fx * ax;
                        p.vy = p._vy + p.fy * ax;
                    }
                }

                // enfore bounds
                if (_bounds) enforceBounds();
            }

            private function enforceBounds():void {
                var minX:Number = _bounds.x;
                var maxX:Number = _bounds.x + _bounds.width;
                var minY:Number = _bounds.y;
                var maxY:Number = _bounds.y + _bounds.height;

                for each (var p:Particle in _particles) {
                    if (p.x < minX) {
                        p.x = minX; p.vx = 0;
                    } else if (p.x > maxX) {
                        p.x = maxX; p.vx = 0;
                    }
                    if (p.y < minY) {
                        p.y = minY; p.vy = 0;
                    }
                    else if (p.y > maxY) {
                        p.y = maxY; p.vy = 0;
                    }
                }
            }

            /**
             * Evaluates the set of forces in the simulation.
             */
            public function eval():void {
                var i:uint, p:Particle;
                // reset forces
                for (i=_particles.length; --i >= 0; ) {
                    p = _particles[i];
                    p.fx = p.fy = 0;
                }
                // collect forces
                for (i=0; i<_forces.length; ++i) {
                    IForce(_forces[i]).apply(this);
                }
            }

            // -- Particle Pool ---------------------------------------------------

            /** The maximum number of items stored in a simulation object pool. */
            public static var objectPoolLimit:int = 5000;
            protected static var _ppool:Array = new Array();
            protected static var _spool:Array = new Array();

            /**
             * Returns a particle instance, pulling a recycled particle from the
             * object pool if available.
             * @param mass the mass (charge) of the particle
             * @param x the particle's starting x position
             * @param y the particle's starting y position
             * @return a particle instance
             */
            protected static function getParticle(mass:Number, x:Number, y:Number):Particle
            {
                if (_ppool.length > 0) {
                    var p:Particle = _ppool.pop();
                    p.init(mass, x, y);
                    return p;
                } else {
                    return new Particle(mass, x, y);
                }
            }

            /**
             * Returns a spring instance, pulling a recycled spring from the
             * object pool if available.
             * @param p1 the first particle attached to the spring
             * @param p2 the second particle attached to the spring
             * @param restLength the rest length of the spring
             * @param tension the tension of the spring
             * @param damping the damping (friction) co-efficient of the spring
             * @return a spring instance
             */
            protected static function getSpring(p1:Particle, p2:Particle,
                restLength:Number, tension:Number, damping:Number):Spring
            {
                if (_spool.length > 0) {
                    var s:Spring = _spool.pop();
                    s.init(p1, p2, restLength, tension, damping);
                    return s;
                } else {
                    return new Spring(p1, p2, restLength, tension, damping);
                }
            }

            /**
             * Reclaims a particle, adding it to the object pool for recycling
             * @param p the particle to reclaim
             */
            protected static function reclaimParticle(p:Particle):void
            {
                if (_ppool.length < objectPoolLimit) {
                    _ppool.push(p);
                }
            }

            /**
             * Reclaims a spring, adding it to the object pool for recycling
             * @param s the spring to reclaim
             */
            protected static function reclaimSpring(s:Spring):void
            {
                if (_spool.length < objectPoolLimit) {
                    _spool.push(s);
                }
            }

        } // end of class Simulation
    }