package flare.animate
{
    import flare.util.Maths;
    
    import flash.events.EventDispatcher;

    [Event("start",  type="flare.animate.TransitionEvent")]
    [Event("step",   type="flare.animate.TransitionEvent")]
    [Event("end",    type="flare.animate.TransitionEvent")]
    [Event("cancel", type="flare.animate.TransitionEvent")]

    /**
     * Base class representing an animated transition. Provides support for
     * tracking animation progress over a time duration. The Transition class
     * also issues events whenever the transition is started, stepped, or
     * ended. Register event listeners for <code>TransitionEvents</code> to
     * track and respond to a transition's progress.
     * 
     * <p>Useful subclasses of <code>Transition</code> include the
     * <code>Tween</code>, <code>Parallel</code>, <code>Sequence</code>,
     * <code>Pause</code>, and <code>Transitioner</code> classes.</p>
     */
    public class Transition extends EventDispatcher implements ISchedulable
    {
        /** Default easing function: a cubic slow-in slow-out. */
        public static var DEFAULT_EASING:Function = Easing.easeInOutPoly(3);
        
        /** Constant indicating this Transition needs initialization. */
        protected static const SETUP:int = 0;
        /** Constant indicating this Transition has been initialized. */
        protected static const INIT:int = 1;
        /** Constant indicating this Transition is currently running. */
        protected static const RUN:int = 2;
        
        // -- Properties ------------------------------------------------------

        private var _easing:Function = DEFAULT_EASING; // easing function
        
        private var _id:String = null;        // transition id, default null
        private var _duration:Number;         // duration, in seconds
        private var _delay:Number;            // delay, in seconds
        private var _frac:Number;             // animation fraction
        private var _state:int = SETUP;       // initialization flag
        /** @private */
        protected var _start:Number;          // start time    
        /** Flag indicating this Transition is currently running. */
        protected var _running:Boolean = false;
        /** Flag indicating this Transition is running in reverse. */
        protected var _reverse:Boolean = false;
        /** Flag indicating if step events should be processed. */
        public var enabled:Boolean = true;
        
        /** @inheritDoc */
        public function get id():String { return _id; }
        public function set id(s:String):void
        {
            if (_running) {
                throw new Error(
                    "The id can't be changed while a transition is running.");
            } else {
                _id = s;
            }
        }
        
        /** The total duration, including both delay and active duration. */
        public function get totalDuration():Number { return duration + delay; }
        
        /** The duration (length) of this Transition, in seconds. */
        public function get duration():Number { return _duration; }
        public function set duration(d:Number):void {
            if (d<0) throw new ArgumentError("Negative duration not allowed.");
            _duration = d;
        }

        /** The delay between a call to play and the actual start
         *  of the transition, in seconds. */
        public function get delay():Number { return _delay; }
        public function set delay(d:Number):void {
            if (d<0) throw new ArgumentError("Negative delay not allowed.");
            _delay = d;
        }
        
        /** Fraction between 0 and 1 indicating the current progress
         *  of this transition. */
        public function get progress():Number { return _frac; }
        internal function set progress(f:Number):void { _frac = f; }
        
        /** Easing function used to pace this Transition. */
        public function get easing():Function { return _easing; }
        public function set easing(f:Function):void { _easing = f; }
        
        /** Indicates if this Transition is currently running. */
        public function get running():Boolean { return _running; }
        
        /** Indicates if this Transition is running in reverse. */
        public function get reverse():Boolean { return _reverse; }
        
        
        // -- Methods ---------------------------------------------------------
        
        /**
         * Creates a new Transition.
         * @param duration the duration, in seconds
         * @param delay the delay, in seconds
         * @param easing the easing function
         */
        public function Transition(duration:Number=1, delay:Number=0,
                                   easing:Function=null)
        {
            _duration = duration;
            _delay = delay;
            _easing = (easing==null ? DEFAULT_EASING : easing);
        }
        
        /**
         * Starts running the transition.
         * @param reverse if true, the transition is played in reverse,
         *  if false (the default), it is played normally.
         */
        public function play(reverse:Boolean = false):void
        {
            _reverse = reverse;
            init();
            Scheduler.instance.add(this);
            _running = true;
        }
        
        /**
         * Stops the transition and completes it.
         * Any end-of-transition actions will still be taken.
         * Calling play() after stop() will result in the transition
         * starting over from the beginning.
         */
        public function stop():void
        {
            Scheduler.instance.remove(this);
            doEnd();
        }
        
        /**
         * Informs this transition that it was cancelled by the scheduler.
         * Assumes that the scheduler has already removed the transition.
         * Clients should not call this method, but should use the
         * <code>stop()</code> method to end a transition early.
         */
        public function cancelled():void
        {
            doEnd(TransitionEvent.CANCEL);
        }
        
        /**
         * Resets the transition, so that any cached starting values are
         * cleared and reset the next time this transition is played.
         */
        public function reset():void
        {
            _state = SETUP;
        }
        
        /**
         * Pauses the transition at its current position.
         * Calling play() after pause() will resume the transition.
         */
        public function pause():void
        {
            Scheduler.instance.remove(this);
            _running = false;
        }
        
        private function init():void
        {
            if (_state == SETUP) doSetup();
            if (_state == RUN) {
                var f:Number = _reverse ? (1-_frac) : _frac;
                _start = new Date().time - f * 1000 * (duration + delay);
            } else {
                _start = new Date().time;
                doStart(_reverse);
            }
            _state = RUN;
        }

        /** @private */
        internal function doSetup():void
        {
            setup();
            _state = INIT;
        }

        /** @private */
        internal function doStart(reverse:Boolean):void
        {
            _reverse = reverse;
            _running = true;
            _frac = _reverse ? 1 : 0;
            start();
            if (hasEventListener(TransitionEvent.START)) {
                dispatchEvent(new TransitionEvent(TransitionEvent.START, this));
            }
        }
        
        /** @private */
        internal function doStep(frac:Number):void
        {
            if (!enabled) return;
            _frac = frac;
            var f:Number = delay==0 || frac==0 ? frac :
                Maths.invLinearInterp(frac, delay/totalDuration, 1);
            if (f >= 0) step(_easing(f));
            if (hasEventListener(TransitionEvent.STEP)) {
                dispatchEvent(new TransitionEvent(TransitionEvent.STEP, this));
            }
        }
        
        /** @private */
        internal function doEnd(evtType:String=TransitionEvent.END):void
        {
            _frac = _reverse ? 0 : 1;
            end();
            _state = INIT;
            _running = false;
            if (hasEventListener(evtType)) {
                dispatchEvent(new TransitionEvent(evtType, this));
            }
        }
        
        /**
         * Evaluates the Transition, stepping the transition forward.
         * @param time the current time in milliseconds
         * @return true if this item should be removed from the scheduler,
         * false if it should continue to be run.
         */
        public function evaluate(time:Number):Boolean
        {
            var t:Number = time - _start;
            if (t < 0) return false;
            
            // step the transition forward
            var d:Number = 1000 * (duration + delay);
            t = (d==0 ? 1.0 : t/d);
            if (t > 1) t = 1; // clamp
            doStep(_reverse ? 1-t : t);
            
            // check if we're done
            var _done:Boolean = (t >= 1.0);
            if (_done) { doEnd(); }
            return _done;
        }
        
        /**
         * Disposes of this transition, freeing up any resources held. This
         * method is optional, but calling it when a transition is no longer
         * needed can help improve overall performance.
         */
        public function dispose():void
        {
            // for sub-classes to implement
        }
        
        // -- abstract methods ------------------------------------------------
        
        /**
         * Transition setup routine. Subclasses should override this function
         * to perform custom setup actions.
         */
        protected function setup():void
        {
            // for sub-classes to implement
        }
        
        /**
         * Transition start routine. Subclasses should override this function
         * to perform custom start actions.
         */
        protected function start():void
        {
            // for sub-classes to implement
        }
        
        /**
         * Transition step routine. Subclasses should override this function
         * to perform custom step actions.
         */
        internal function step(ef:Number):void
        {
            // for sub-classes to implement
        }
        
        /**
         * Transition end routine. Subclasses should override this function
         * to perform custom ending actions.
         */
        protected function end():void
        {
            // for sub-classes to implement
        }
        
    } // end of class Transition
}