package net.flashpunk.graphics 
{
	import flash.display.BitmapData;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import net.flashpunk.Graphic;
	import net.flashpunk.FP;
	import net.flashpunk.masks.Grid;
	
	/**
	 * A canvas to which Tiles can be drawn for fast multiple tile rendering.
	 */
	public class Tilemap extends Canvas
	{
		/**
		 * If x/y positions should be used instead of columns/rows.
		 */
		public var usePositions:Boolean;
		
		/**
		 * Constructor.
		 * @param	tileset			The source tileset image.
		 * @param	width			Width of the tilemap, in pixels.
		 * @param	height			Height of the tilemap, in pixels.
		 * @param	tileWidth		Tile width.
		 * @param	tileHeight		Tile height.
		 */
		public function Tilemap(tileset:*, width:uint, height:uint, tileWidth:uint, tileHeight:uint) 
		{
			// set some tilemap information
			_width = width - (width % tileWidth);
			_height = height - (height % tileHeight);
			_columns = _width / tileWidth;
			_rows = _height / tileHeight;
			_map = new BitmapData(_columns, _rows, false, 0);
			_temp = _map.clone();
			_tile = new Rectangle(0, 0, tileWidth, tileHeight);
			
			// create the canvas
			_maxWidth -= _maxWidth % tileWidth;
			_maxHeight -= _maxHeight % tileHeight;
			super(_width, _height);
			
			// load the tileset graphic
			if (tileset is Class) _set = FP.getBitmap(tileset);
			else if (tileset is BitmapData) _set = tileset;
			if (!_set) throw new Error("Invalid tileset graphic provided.");
			_setColumns = uint(_set.width / tileWidth);
			_setRows = uint(_set.height / tileHeight);
			_setCount = _setColumns * _setRows;
		}
		
		/**
		 * Sets the index of the tile at the position.
		 * @param	column		Tile column.
		 * @param	row			Tile row.
		 * @param	index		Tile index.
		 */
		public function setTile(column:uint, row:uint, index:uint = 0):void
		{
			if (usePositions)
			{
				column /= _tile.width;
				row /= _tile.height;
			}
			index %= _setCount;
			column %= _columns;
			row %= _rows;
			_tile.x = (index % _setColumns) * _tile.width;
			_tile.y = uint(index / _setColumns) * _tile.height;
			_map.setPixel(column, row, index);
			draw(column * _tile.width, row * _tile.height, _set, _tile);
		}
		
		/**
		 * Clears the tile at the position.
		 * @param	column		Tile column.
		 * @param	row			Tile row.
		 */
		public function clearTile(column:uint, row:uint):void
		{
			if (usePositions)
			{
				column /= _tile.width;
				row /= _tile.height;
			}
			column %= _columns;
			row %= _rows;
			_tile.x = column * _tile.width;
			_tile.y = row * _tile.height;
			fill(_tile, 0, 0);
		}
		
		/**
		 * Gets the tile index at the position.
		 * @param	column		Tile column.
		 * @param	row			Tile row.
		 * @return	The tile index.
		 */
		public function getTile(column:uint, row:uint):uint
		{
			if (usePositions)
			{
				column /= _tile.width;
				row /= _tile.height;
			}
			return _map.getPixel(column % _columns, row % _rows);
		}
		
		/**
		 * Sets a rectangular region of tiles to the index.
		 * @param	column		First tile column.
		 * @param	row			First tile row.
		 * @param	width		Width in tiles.
		 * @param	height		Height in tiles.
		 * @param	index		Tile index.
		 */
		public function setRect(column:uint, row:uint, width:uint = 1, height:uint = 1, index:uint = 0):void
		{
			if (usePositions)
			{
				column /= _tile.width;
				row /= _tile.height;
				width /= _tile.width;
				height /= _tile.height;
			}
			column %= _columns;
			row %= _rows;
			var c:uint = column,
				r:uint = column + width,
				b:uint = row + height,
				u:Boolean = usePositions;
			usePositions = false;
			while (row < b)
			{
				while (column < r)
				{
					setTile(column, row, index);
					column ++;
				}
				column = c;
				row ++;
			}
			usePositions = u;
		}
		
		/**
		 * Clears the rectangular region of tiles.
		 * @param	column		First tile column.
		 * @param	row			First tile row.
		 * @param	width		Width in tiles.
		 * @param	height		Height in tiles.
		 */
		public function clearRect(column:uint, row:uint, width:uint = 1, height:uint = 1):void
		{
			if (usePositions)
			{
				column /= _tile.width;
				row /= _tile.height;
				width /= _tile.width;
				height /= _tile.height;
			}
			column %= _columns;
			row %= _rows;
			var c:uint = column,
				r:uint = column + width,
				b:uint = row + height,
				u:Boolean = usePositions;
			usePositions = false;
			while (row < b)
			{
				while (column < r)
				{
					clearTile(column, row);
					column ++;
				}
				column = c;
				row ++;
			}
			usePositions = u;
		}
		
		/**
		* Loads the Tilemap tile index data from a string.
		* @param str			The string data, which is a set of tile values separated by the columnSep and rowSep strings.
		* @param columnSep		The string that separates each tile value on a row, default is ",".
		* @param rowSep			The string that separates each row of tiles, default is "\n".
		*/
		public function loadFromString(str:String, columnSep:String = ",", rowSep:String = "\n"):void
		{
			var row:Array = str.split(rowSep),
				rows:int = row.length,
				col:Array, cols:int, x:int, y:int;
			for (y = 0; y < rows; y ++)
			{
				if (row[y] == '') continue;
				col = row[y].split(columnSep),
				cols = col.length;
				for (x = 0; x < cols; x ++)
				{
					if (col[x] == '') continue;
					setTile(x, y, uint(col[x]));
				}
			}
		}
		
		/**
		* Saves the Tilemap tile index data to a string.
		* @param columnSep		The string that separates each tile value on a row, default is ",".
		* @param rowSep			The string that separates each row of tiles, default is "\n".
		*/
		public function saveToString(columnSep:String = ",", rowSep:String = "\n"): String
		{
			var s:String = '',
				x:int, y:int;
			for (y = 0; y < _rows; y ++)
			{
				for (x = 0; x < _columns; x ++)
				{
					s += String(getTile(x, y));
					if (x != _columns - 1) s += columnSep;
				}
				if (y != _rows - 1) s += rowSep;
			}
			return s;
		}
		
		/**
		 * Gets the index of a tile, based on its column and row in the tileset.
		 * @param	tilesColumn		Tileset column.
		 * @param	tilesRow		Tileset row.
		 * @return	Index of the tile.
		 */
		public function getIndex(tilesColumn:uint, tilesRow:uint):uint
		{
			return (tilesRow % _setRows) * _setColumns + (tilesColumn % _setColumns);
		}
		
		/**
		 * Shifts all the tiles in the tilemap.
		 * @param	columns		Horizontal shift.
		 * @param	rows		Vertical shift.
		 * @param	wrap		If tiles shifted off the canvas should wrap around to the other side.
		 */
		public function shiftTiles(columns:int, rows:int, wrap:Boolean = false):void
		{
			if (usePositions)
			{
				columns /= _tile.width;
				rows /= _tile.height;
			}
			
			if (!wrap) _temp.fillRect(_temp.rect, 0);
			
			if (columns != 0)
			{
				shift(columns * _tile.width, 0);
				if (wrap) _temp.copyPixels(_map, _map.rect, FP.zero);
				_map.scroll(columns, 0);
				_point.y = 0;
				_point.x = columns > 0 ? columns - _columns : columns + _columns;
				_map.copyPixels(_temp, _temp.rect, _point);
				
				_rect.x = columns > 0 ? 0 : _columns + columns;
				_rect.y = 0;
				_rect.width = Math.abs(columns);
				_rect.height = _rows;
				updateRect(_rect, !wrap);
			}
			
			if (rows != 0)
			{
				shift(0, rows * _tile.height);
				if (wrap) _temp.copyPixels(_map, _map.rect, FP.zero);
				_map.scroll(0, rows);
				_point.x = 0;
				_point.y = rows > 0 ? rows - _rows : rows + _rows;
				_map.copyPixels(_temp, _temp.rect, _point);
				
				_rect.x = 0;
				_rect.y = rows > 0 ? 0 : _rows + rows;
				_rect.width = _columns;
				_rect.height = Math.abs(rows);
				updateRect(_rect, !wrap);
			}
		}
		
		/** @private Used by shiftTiles to update a rectangle of tiles from the tilemap. */
		private function updateRect(rect:Rectangle, clear:Boolean):void
		{
			var x:int = rect.x,
				y:int = rect.y,
				w:int = x + rect.width,
				h:int = y + rect.height,
				u:Boolean = usePositions;
			usePositions = false;
			if (clear)
			{
				while (y < h)
				{
					while (x < w) clearTile(x ++, y);
					x = rect.x;
					y ++;
				}
			}
			else
			{
				while (y < h)
				{
					while (x < w) updateTile(x ++, y);
					x = rect.x;
					y ++;
				}
			}
			usePositions = u;
		}
		
		/** @private Used by shiftTiles to update a tile from the tilemap. */
		private function updateTile(column:uint, row:uint):void
		{
			setTile(column, row, _map.getPixel(column % _columns, row % _rows));
		}
		
		/**
		 * The tile width.
		 */
		public function get tileWidth():uint { return _tile.width; }
		
		/**
		 * The tile height.
		 */
		public function get tileHeight():uint { return _tile.height; }
		
		/**
		 * How many columns the tilemap has.
		 */
		public function get columns():uint { return _columns; }
		
		/**
		 * How many rows the tilemap has.
		 */
		public function get rows():uint { return _rows; }
		
		// Tilemap information.
		/** @private */ private var _map:BitmapData;
		/** @private */ private var _temp:BitmapData;
		/** @private */ private var _columns:uint;
		/** @private */ private var _rows:uint;
		
		// Tileset information.
		/** @private */ private var _set:BitmapData;
		/** @private */ private var _setColumns:uint;
		/** @private */ private var _setRows:uint;
		/** @private */ private var _setCount:uint;
		/** @private */ private var _tile:Rectangle;
		
		// Global objects.
		/** @private */ private var _rect:Rectangle = FP.rect;
	}
}