/*******************************************************************************
 * Copyright (c) 2012 by Thomas Jahn
 * Questions? Mail me at lithander@gmx.de!
 ******************************************************************************/
package
{
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.events.MouseEvent;
	import flash.geom.Rectangle;
	import flash.text.AntiAliasType;
	import flash.text.TextField;
	import flash.text.TextFormat;
	import flash.ui.Keyboard;
	import flash.utils.Timer;
	import flash.utils.getTimer;
	
	import net.pixelpracht.geometry.Angle;
	import net.pixelpracht.geometry.Rectangle2D;
	import net.pixelpracht.geometry.Vector2D;
	
	[SWF(width="320", height="340", backgroundColor="#000000")]
	public class FieldOfViewDemo extends Sprite
	{
		private var _width:int = 0;
		private var _height:int = 0;
		private var _cellsize:int = 0;
		private var _grid:Vector.<LightGridData> = null;
		private var _buffer:BitmapData = null;
		private var _clearColor:uint = 0x241509;
		private var _screen:Bitmap = null;
		
		private var _mode:int = 0;
		private var _moveLight:Boolean = true;
		private var _modeLabel:TextField = null;
		private var _ambientLight:uint = 0x403020;
		private var _lightPos:Vector2D = new Vector2D();
		private var _tmp:Vector2D = new Vector2D();
		private var _wideRays:Boolean = false;
		
		public function FieldOfViewDemo()
		{
			stage.frameRate = 30;
			//defer all initialization until ADDED_TO_STAGE event is received
			addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
			addEventListener(Event.ENTER_FRAME, onUpdate);
		}
		
		private function onAddedToStage(e:Event):void
		{
			//register input event handler
			stage.addEventListener(MouseEvent.MOUSE_DOWN, onScreenClicked);
			stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
			stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
			//init a grid with 32x32 square cells
			initGrid(32, 32);
			//init a buffer to render the grind in - 10 pixel per square
			initBuffer(10);
			//init a textField to display currently selected mode
			initUI();
		}
				
		private function onUpdate(e:Event):void
		{
			//set the color of each cell to _ambientLight
			clearGrid();
			
			var range:int = 10; //max range of FoV
			var color:uint = 0xD0D0B0;//0xB0B070;
			var baseColor:uint = 0x000000;//0x202040;
			var status:String = "";
			var dt:Number = getTimer();
			//depending on what _mode is currently selected use different algorithm to calc FoV
			switch(_mode)
			{
				case 0:
					status = "Partial Occlusion Recursive";
					addShadowLightRecursive(_lightPos, range, color);
					break;
				case 1:
					status = "Partial Occlusion Iterator";
					addShadowLight(_lightPos, range, color);
					break;
				case 2:
					status = "Raycast: Narrow";
					untouch(_lightPos, range);
					castRays(_lightPos, range, true, 1);
					addLightIfTouched(_lightPos, range, color, 1);	
					break;
				case 3:
					status = "Raycast: Wide";
					untouch(_lightPos, range);
					castRays(_lightPos, range, false, 1);
					addLightIfTouched(_lightPos, range, color, 1);	
					break;
				case 4:
					status = "Raycast: Narrow AND Wide";
					untouch(_lightPos, range);
					//cast rays (once wide and once narrow) with different masks
					castRays(_lightPos, range, true, 1);
					castRays(_lightPos, range, false, 2);
					//shade cells that were touched by both ray-types
					addLightIfTouched(_lightPos, range, color, 3);	
					break;
				case 5:
					status = "Raycast: Narrow vs Wide";
					untouch(_lightPos, range);
					//cast rays (once wide and once narrow) with different masks
					castRays(_lightPos, range, true, 1);
					castRays(_lightPos, range, false, 2);
					//shade cells depending on what rays touched them
					addLightIfTouched(_lightPos, range, color & 0xFF0000, 1);	
					addLightIfTouched(_lightPos, range, color & 0x00FF00, 2);	
					addLightIfTouched(_lightPos, range, color & 0x0000FF, 3);	
					break;		
			}
			//finally render lightgrid to buffer
			renderBuffer();	
			//print mode and a deltaTime estimate
			status += " "+(getTimer() - dt)+"ms";
			_modeLabel.text = status;
		}
		
		//ADD LIGHT - CALC SHADOWS USING RAYCASTS

		private function onKeyDown(event:KeyboardEvent):void
		{
			//Space key to toggle mode
			if(event.keyCode == Keyboard.SPACE)
				_mode = (_mode + 1) % 6;
			//'D' attaches/detaches lightsource from mouse
			if(event.keyCode == 68)
				_moveLight = !_moveLight;
		}
				
		private function onScreenClicked(event:MouseEvent):void
		{			
			var cellX:int = _screen.mouseX / _cellsize;
			var cellY:int = _screen.mouseY / _cellsize;
			handleClick(cellX, cellY);
		}
		
		private function onMouseMove(event:MouseEvent):void
		{
			//if lightsource is attached to mouse, move lightsource accordingly
			if(_moveLight)
			{
				_lightPos.x = _screen.mouseX / _cellsize;
				_lightPos.y = _screen.mouseY / _cellsize;
			}
		}
		
		private function initUI():void
		{
			//create a textField to display status message
			var myFormat:TextFormat = new TextFormat();
			myFormat.size = 16;
			myFormat.font = "Courier New";
			_modeLabel = new TextField();
			_modeLabel.width = stage.width;
			_modeLabel.textColor = 0xFFFF66;
			_modeLabel.defaultTextFormat = myFormat;
			_modeLabel.antiAliasType = AntiAliasType.ADVANCED;
			_modeLabel.text = "Mode";
			stage.addChild(_modeLabel);
		}
		
		private function initGrid(width:int, height:int):void
		{
			_width = width;
			_height = height;
			_grid = new Vector.<LightGridData>(_width * _height, true);

			//create lightGrid nodes
			for(var y:int = 0; y < _height; y++)
				for(var x:int = 0; x < _width; x++)
					_grid[y * _width + x] = new LightGridData(x, y);
			
			//connect lightGrid nodes
			for(y = 0; y < _height; y++)
				for(x = 0; x < _width; x++)
				{
					var ld:LightGridData = _grid[y * _width + x];
					ld.left = fetch(x-1,y);
					ld.right = fetch(x+1,y);
					ld.up = fetch(x,y-1);
					ld.down = fetch(x,y+1);
				}
		}
		
		private function clearGrid():void
		{
			for(var i:int = 0; i < _width * _height; i++)
				_grid[i].clear(_ambientLight);
		}
		
		private function initBuffer(cellsize:int):void
		{
			//create a buffer of sufficient size
			_cellsize = cellsize;
			_buffer = new BitmapData(_width*cellsize, _height*cellsize, false, _clearColor);
			//create displayobject to display buffer on stage
			_screen = new Bitmap(_buffer);
			_screen.y = 20;
			//add displayobject to stage
			stage.addChild(_screen);			
		}
		
		private function renderBuffer():void
		{
			//clear previous content
			_buffer.fillRect(_buffer.rect, _clearColor);
			
			//render light-grid
			var rect:Rectangle = new Rectangle(0,0,_cellsize-2,_cellsize-2);
			for(var y:int = 0; y < _height; y++)
				for(var x:int = 0; x < _width; x++)
				{
					rect.x = x*_cellsize+1;
					rect.y = y*_cellsize+1;
					_buffer.fillRect(rect, fetch(x,y).color);
				}
			
			//render lightsource as a little x
			var px:int = (_lightPos.x * _cellsize);
			var py:int = (_lightPos.y * _cellsize);
			for(x = -3; x <= 3; x++)
				_buffer.setPixel(px+x, py, 0x000000);
			for(y = -3; y <= 3; y++)
				_buffer.setPixel(px, py+y, 0x000000);
		}		
		
		//just some utility method to provide acces to the lightgrid without worrying about causing index-out-of-bound exceptions
		private function fetch(x:int, y:int):LightGridData
		{
			if(x >= 0 && x < _width && y >= 0 && y < _height)
				return _grid[y * _width + x];
			else
				return null;
		}
				
		private function handleClick(cX:int, cY:int):void
		{
			//clicking a cell toggles it's state from opaque to transparent
			var ld:LightGridData = fetch(cX, cY);
			if(ld != null)
				ld.opaque = !ld.opaque;			
		}
		
		
		//ADD LIGHT - all clear cells receive light, no occlusion is considered	
		private function addLight(pos:Vector2D, range:Number, color:uint):void
		{
			var rSqr:Number = range * range;
			var minx:int = Math.max(0, pos.x - range);
			var maxx:int = Math.min(_width-1, pos.x + range);
			var miny:int = Math.max(0, pos.y - range);
			var maxy:int = Math.min(_height-1, pos.y + range);
			for(var y:int = miny; y <= maxy; y++)
				for(var x:int = minx; x <= maxx; x++)
				{
					var ld:LightGridData = _grid[y * _width + x];
					if(ld.opaque)
						continue;
					
					var dSqr:Number = pos.distanceSquared(_tmp.reset(x+0.5, y+0.5));
					var intensity:Number = Math.max(0.0, 1.0 - dSqr / rSqr);
					ld.addScaled(color, intensity);
				}
		}
		
		//ADD LIGHT - RAYCAST
		//trigger raycast to each cell on the bounding rectangle of the FOV
		private function castRays(pos:Vector2D, range:Number, allowDiagonalSteps:Boolean, touchMask:uint):void
		{
			var rCeil:int = Math.ceil(range);
			var dx:Number = -rCeil;
			var dy:Number = -rCeil;
			while (dx < rCeil)
				castRay(pos, range, dx++, dy, allowDiagonalSteps, touchMask);
			while (dy < rCeil)
				castRay(pos, range, dx, dy++, allowDiagonalSteps, touchMask);
			while (dx > -rCeil)
				castRay(pos, range, dx--, dy, allowDiagonalSteps, touchMask);
			while (dy > -rCeil)
				castRay(pos, range, dx, dy--, allowDiagonalSteps, touchMask);
		}
		
		//instead of calculating occlusion directly set bit flags in the touched mask when a cell is hit by raycasts
		private function castRay(pos:Vector2D, range:Number, dirX:int, dirY:int, allowDiagonalSteps:Boolean, touchMask:uint):Boolean
		{
			var rCeil:int = Math.ceil(range);
			var x:int = pos.x;
			var y:int = pos.y;
			
			var dx:int = Math.abs(dirX);
			var dy:int = Math.abs(dirY);
			var sx:int = (dirX > 0) ? 1 : -1; //sign
			var sy:int = (dirY > 0) ? 1 : -1; //sign
			var err:int = dx - dy;
			var xSum:int = 0;
			var ySum:int = 0;
			
			//move startX, startY to targetX, targetY
			while (true)
			{
				//max range reached?
				if (xSum * xSum + ySum * ySum >= range * range)
					break;
				
				//check for collsion
				var ld:LightGridData = fetch(x, y); //TODO: optimize - avoid function call
				if (ld == null || ld.opaque)
					return false;
				
				//touch! set flags as specified in the touchMask
				ld.touched |= touchMask;
				
				var e2:int = (2 * err);
				if(allowDiagonalSteps == false)
				{
					//either step vertically or horizontal, based on 
					//wether deviaton from the ideal line is currently bigger
					//in x- or y-direction.
					if (e2 > -dy && Math.abs(e2 + dy) > Math.abs(e2 - dx))
					{
						xSum++;
						err -= dy;
						x += sx;
					}
					else if (e2 < dx)
					{
						ySum++;
						err += dx;
						y += sy;
					}					
				}
				else
				{
					//diagonal movements are allowed too. Just step, when error big enough!
					if (e2 > -dy)
					{
						xSum++;
						err -= dy;
						x += sx;
					}
					if (e2 < dx)
					{
						ySum++;
						err += dx;
						y += sy;
					}
				}
			}
			return true;
		}
		
		//clear touched mask
		private function untouch(pos:Vector2D, range:Number):void
		{
			var minx:int = Math.max(0, pos.x - range);
			var maxx:int = Math.min(_width-1, pos.x + range);
			var miny:int = Math.max(0, pos.y - range);
			var maxy:int = Math.min(_height-1, pos.y + range);
			for(var y:int = miny; y <= maxy; y++)
				for(var x:int = minx; x <= maxx; x++)
					_grid[y * _width + x].touched = 0;			
		}
		
		//add light for each cell that has all bitflags specified by touchMask set
		private function addLightIfTouched(pos:Vector2D, range:Number, color:uint, touchMask:uint):void
		{
			var rSqr:Number = range * range;
			var minx:int = Math.max(0, pos.x - range);
			var maxx:int = Math.min(_width-1, pos.x + range);
			var miny:int = Math.max(0, pos.y - range);
			var maxy:int = Math.min(_height-1, pos.y + range);
			for(var y:int = miny; y <= maxy; y++)
				for(var x:int = minx; x <= maxx; x++)
				{
					var ld:LightGridData = _grid[y * _width + x];
					if((ld.touched & touchMask) == touchMask)
					{
						//cell receives light - intensity calculated based on distance from lightsource
						var dSqr:Number = pos.distanceSquared(_tmp.reset(x+0.5, y+0.5));
						var intensity:Number = Math.max(0.0, 1.0 - dSqr / rSqr);
						ld.addScaled(color, intensity);
						//TODO: could clear touched here instead of in a sperate step
					}
				}
		}		
		
		//PARTIAL OCCLUSION FIELD OV FIEW
		private function addShadowLightRecursive(pos:Vector2D, range:Number, color:uint):void
		{
			//this is the recursive variant of the algorithm
			var minx:int = Math.max(0, pos.x - range);
			var maxx:int = Math.min(_width-1, pos.x + range);
			var miny:int = Math.max(0, pos.y - range);
			var maxy:int = Math.min(_height-1, pos.y + range);
			
			var centerY:int = Math.max(0, Math.ceil(pos.y) - 1);
			var centerX:int = Math.max(0, Math.ceil(pos.x) - 1);
			
			//reset occlusion
			for(var y:int = miny; y <= maxy; y++)
				for(var x:int = minx; x <= maxx; x++)
					_grid[y * _width + x].resetOcclusion();	
			
			//calculate occlusion by starting from the edges, eventually visiting all cells in the relevant rect
			fetch(minx, miny).calclOcclusionRecursive(pos, true);
			fetch(minx, maxy).calclOcclusionRecursive(pos, true);
			
			//clear center (necessary HACK becuse left and right marching eval uses different Angle-normalization)
			for(y = miny; y <= centerY; y++)
				_grid[y * _width + centerX].resetOcclusion();			
			
			fetch(maxx, miny).calclOcclusionRecursive(pos, false);
			fetch(maxx, maxy).calclOcclusionRecursive(pos, false);
			
			//shade based on occlusion & distance from light source
			var rSqr:Number = range * range;
			for(y = miny; y <= maxy; y++)
				for(x = minx; x <= maxx; x++)
				{
					//cell receives light based on intensity and occlusion
					//intensity is calculated based on distance from lightsource
					var dSqr:Number = pos.distanceSquared(_tmp.reset(x+0.5, y+0.5));
					var intensity:Number = Math.max(0.0, 1.0 - dSqr / rSqr);
					var ld:LightGridData = _grid[y * _width + x];
					ld.addScaled(color, intensity * (1 - ld.occlusion));
				}			
		}
		
		private function addShadowLight(pos:Vector2D, range:int, color:uint):void
		{
			//this is the iterating variant of the algorithm
			var minx:int = Math.max(0, pos.x - range);
			var maxx:int = Math.min(_width-1, pos.x + range);
			var miny:int = Math.max(0, pos.y - range);
			var maxy:int = Math.min(_height-1, pos.y + range);
			
			//the iterator pattern we need is a little more complex then your average FOR-loop...
			var px:int = Math.ceil(pos.x) - 1;
			var py:int = Math.ceil(pos.y) - 1;
			var maxi:int = range*Math.SQRT2;
			for(var i:int = 0; i <= maxi; i++)
				for(var j:int = Math.max(0, i-range); j <= i && j <= range; j++)
				{
					//West->North
					updateOcclusion(px-i+j, py-j, pos, true);
					//Sout->West
					updateOcclusion(px-i+j, py+j, pos, true);
				}		
			
			for(i = 0; i <= maxi; i++)
				for(j = Math.max(0, i-range); j <= i && j <= range; j++)
				{
					//North->Eeast
					updateOcclusion(px+j, py-i+j, pos, false);
					//East->South
					updateOcclusion(px-j+i, py+j, pos, false);
				}

			
			//shade based on occlusion & distance from light source
			var rSqr:Number = range * range;
			for(y = miny; y <= maxy; y++)
				for(x = minx; x <= maxx; x++)
				{
					var dSqr:Number = pos.distanceSquared(_tmp.reset(x+0.5, y+0.5));
					var intensity:Number = Math.max(0.0, 1.0 - dSqr / rSqr);
					var ld:LightGridData = _grid[y * _width + x];
					ld.addScaled(color, intensity * (1 - ld.occlusion));
				}			
		}
		
		private function updateOcclusion(x:int, y:int, pos:Vector2D, normalize:Boolean):void
		{
			if(x >= 0 && x < _width && y >= 0 && y < _height)
				_grid[y * _width + x].calcOcclusion(pos, normalize);
		}

	}
}