package net.flashpunk
{
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.MovieClip;
	import flash.display.StageAlign;
	import flash.display.StageDisplayState;
	import flash.display.StageQuality;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.events.TimerEvent;
	import flash.geom.Rectangle;
	import flash.utils.getTimer;
	import flash.utils.Timer;
	import net.flashpunk.utils.Draw;
	import net.flashpunk.utils.Input;
	
	/**
	 * Main game Sprite class, added to the Flash Stage. Manages the game loop.
	 */
	public class Engine extends MovieClip
	{
		/**
		 * If the game should stop updating/rendering.
		 */
		public var paused:Boolean = false;
		
		/**
		 * Cap on the elapsed time (default at 30 FPS). Raise this to allow for lower framerates (eg. 1 / 10).
		 */
		public var maxElapsed:Number = 0.0333;
		
		/**
		 * The max amount of frames that can be skipped in fixed framerate mode.
		 */
		public var maxFrameSkip:uint = 5;
		
		/**
		 * The amount of milliseconds between ticks in fixed framerate mode.
		 */
		public var tickRate:uint = 4;
		
		/**
		 * Constructor. Defines startup information about your game.
		 * @param	width			The width of your game.
		 * @param	height			The height of your game.
		 * @param	frameRate		The game framerate, in frames per second.
		 * @param	fixed			If a fixed-framerate should be used.
		 */
		public function Engine(width:uint, height:uint, frameRate:Number = 60, fixed:Boolean = false) 
		{
			// global game properties
			FP.width = width;
			FP.height = height;
			FP.assignedFrameRate = frameRate;
			FP.fixed = fixed;
			
			// global game objects
			FP.engine = this;
			FP.screen = new Screen;
			FP.bounds = new Rectangle(0, 0, width, height);
			FP._world = new World;
			
			// miscellanious startup stuff
			if (FP.randomSeed == 0) FP.randomizeSeed();
			FP.entity = new Entity;
			FP._time = getTimer();
			
			// on-stage event listener
			addEventListener(Event.ADDED_TO_STAGE, onStage);
		}
		
		/**
		 * Override this, called after Engine has been added to the stage.
		 */
		public function init():void
		{
			
		}
		
		/**
		 * Updates the game, updating the World and Entities.
		 */
		public function update():void
		{
			if (FP._world.active)
			{
				if (FP._world._tween) FP._world.updateTweens();
				FP._world.update();
			}
			FP._world.updateLists();
			if (FP._goto) checkWorld();
		}
		
		/**
		 * Renders the game, rendering the World and Entities.
		 */
		public function render():void
		{
			// timing stuff
			var t:Number = getTimer();
			if (!_frameLast) _frameLast = t;
			
			// render loop
			FP.screen.swap();
			Draw.resetTarget();
			FP.screen.refresh();
			if (FP._world.visible) FP._world.render();
			FP.screen.redraw();
			
			// more timing stuff
			t = getTimer();
			_frameListSum += (_frameList[_frameList.length] = t - _frameLast);
			if (_frameList.length > 10) _frameListSum -= _frameList.shift();
			FP.frameRate = 1000 / (_frameListSum / _frameList.length);
			_frameLast = t;
		}
		
		/**
		 * Sets the game's stage properties. Override this to set them differently.
		 */
		public function setStageProperties():void
		{
			stage.frameRate = FP.assignedFrameRate;
			stage.align = StageAlign.TOP_LEFT;
			stage.quality = StageQuality.HIGH;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.displayState = StageDisplayState.NORMAL;
		}
		
		/** @private Event handler for stage entry. */
		private function onStage(e:Event = null):void
		{
			// remove event listener
			removeEventListener(Event.ADDED_TO_STAGE, onStage);
			
			// set stage properties
			FP.stage = stage;
			setStageProperties();
			
			// enable input
			Input.enable();
			
			// switch worlds
			if (FP._goto) checkWorld();
			
			// game start
			init();
			
			// start game loop
			_rate = 1000 / FP.assignedFrameRate;
			if (FP.fixed)
			{
				// fixed framerate
				_skip = _rate * maxFrameSkip;
				_last = _prev = getTimer();
				_timer = new Timer(tickRate);
				_timer.addEventListener(TimerEvent.TIMER, onTimer);
				_timer.start();
			}
			else
			{
				// nonfixed framerate
				_last = getTimer();
				addEventListener(Event.ENTER_FRAME, onEnterFrame);
			}
		}
		
		/** @private Framerate independent game loop. */
		private function onEnterFrame(e:Event):void
		{
			// update timer
			_time = _gameTime = getTimer();
			FP._flashTime = _time - _flashTime;
			_updateTime = _time;
			FP.elapsed = (_time - _last) / 1000;
			if (FP.elapsed > maxElapsed) FP.elapsed = maxElapsed;
			FP.elapsed *= FP.rate;
			_last = _time;
			
			// update console
			if (FP._console) FP._console.update();
			
			// update loop
			if (!paused) update();
			
			// update input
			Input.update();
			
			// update timer
			_time = _renderTime = getTimer();
			FP._updateTime = _time - _updateTime;
			
			// render loop
			if (!paused) render();
			
			// update timer
			_time = _flashTime = getTimer();
			FP._renderTime = _time - _renderTime;
			FP._gameTime = _time - _gameTime;
		}
		
		/** @private Fixed framerate game loop. */
		private function onTimer(e:TimerEvent):void
		{
			// update timer
			_time = getTimer();
			_delta += (_time - _last);
			_last = _time;
			
			// quit if a frame hasn't passed
			if (_delta < _rate) return;
			
			// update timer
			_gameTime = _time;
			FP._flashTime = _time - _flashTime;
			
			// update console
			if (FP._console) FP._console.update();
			
			// update loop
			if (_delta > _skip) _delta = _skip;
			while (_delta > _rate)
			{
				// update timer
				_updateTime = _time;
				_delta -= _rate;
				FP.elapsed = (_time - _prev) / 1000;
				if (FP.elapsed > maxElapsed) FP.elapsed = maxElapsed;
				FP.elapsed *= FP.rate;
				_prev = _time;
				
				// update loop
				if (!paused) update();
				
				// update input
				Input.update();
				
				// update timer
				_time = getTimer();
				FP._updateTime = _time - _updateTime;
			}
			
			// update timer
			_renderTime = _time;
			
			// render loop
			if (!paused) render();
			
			// update timer
			_time = _flashTime = getTimer();
			FP._renderTime = _time - _renderTime;
			FP._gameTime =  _time - _gameTime;
		}
		
		/** @private Switch Worlds if they've changed. */
		private function checkWorld():void
		{
			if (!FP._goto) return;
			FP._world.end();
			FP._world.updateLists();
			if (FP._world && FP._world.autoClear && FP._world._tween) FP._world.clearTweens();
			FP._world = FP._goto;
			FP._goto = null;
			FP.camera = FP._world.camera;
			FP._world.updateLists();
			FP._world.begin();
			FP._world.updateLists();
		}
		
		// Timing information.
		/** @private */ private var _delta:Number = 0;
		/** @private */ private var _time:Number;
		/** @private */ private var _last:Number;
		/** @private */ private var _timer:Timer;
		/** @private */ private var	_rate:Number;
		/** @private */ private var	_skip:Number;
		/** @private */ private var _prev:Number;
		
		// Debug timing information.
		/** @private */ private var _updateTime:uint;
		/** @private */ private var _renderTime:uint;
		/** @private */ private var _gameTime:uint;
		/** @private */ private var _flashTime:uint;
		
		// FrameRate tracking.
		/** @private */ private var _frameLast:uint = 0;
		/** @private */ private var _frameListSum:uint = 0;
		/** @private */ private var _frameList:Vector.<uint> = new Vector.<uint>;
	}
}