package net.flashpunk.masks
{
    import flash.display.Bitmap;
    import flash.display.BitmapData;
    import flash.geom.Point;
    import flash.geom.Rectangle;
    import net.flashpunk.*;
    
    /**
     * Uses a hash grid to determine collision, faster than
     * using hundreds of Entities for tiled levels, etc.
     */
    public class Grid extends Hitbox
    {
        /**
         * If x/y positions should be used instead of columns/rows.
         */
        public var usePositions:Boolean;
        
        /**
         * Constructor.
         * @param    width            Width of the grid, in pixels.
         * @param    height            Height of the grid, in pixels.
         * @param    tileWidth        Width of a grid tile, in pixels.
         * @param    tileHeight        Height of a grid tile, in pixels.
         * @param    x                X offset of the grid.
         * @param    y                Y offset of the grid.
         */
        public function Grid(width:uint, height:uint, tileWidth:uint, tileHeight:uint, x:int = 0, y:int = 0) 
        {
            // check for illegal grid size
            if (!width || !height || !tileWidth || !tileHeight) throw new Error("Illegal Grid, sizes cannot be 0.");
            
            // set grid properties
            _columns = width / tileWidth;
            _rows = height / tileHeight;
            _data = new BitmapData(_columns, _rows, true, 0);
            _tile = new Rectangle(0, 0, tileWidth, tileHeight);
            _x = x;
            _y = y;
            _width = width;
            _height = height;
            
            // set callback functions
            _check[Mask] = collideMask;
            _check[Hitbox] = collideHitbox;
            _check[Pixelmask] = collidePixelmask;
        }
        
        /**
         * Sets the value of the tile.
         * @param    column        Tile column.
         * @param    row            Tile row.
         * @param    solid        If the tile should be solid.
         */
        public function setTile(column:uint = 0, row:uint = 0, solid:Boolean = true):void
        {
            if (usePositions)
            {
                column /= _tile.width;
                row /= _tile.height;
            }
            _data.setPixel32(column, row, solid ? 0xFFFFFFFF : 0);
        }
        
        /**
         * Makes the tile non-solid.
         * @param    column        Tile column.
         * @param    row            Tile row.
         */
        public function clearTile(column:uint = 0, row:uint = 0):void
        {
            setTile(column, row, false);
        }
        
        /**
         * Gets the value of a tile.
         * @param    column        Tile column.
         * @param    row            Tile row.
         * @return    tile value.
         */
        public function getTile(column:uint = 0, row:uint = 0):Boolean
        {
            if (usePositions)
            {
                column /= _tile.width;
                row /= _tile.height;
            }
            return _data.getPixel32(column, row) > 0;
        }
        
        /**
         * Sets the value of a rectangle region of tiles.
         * @param    column        First column.
         * @param    row            First row.
         * @param    width        Columns to fill.
         * @param    height        Rows to fill.
         * @param    fill        Value to fill.
         */
        public function setRect(column:uint = 0, row:uint = 0, width:int = 1, height:int = 1, solid:Boolean = true):void
        {
            if (usePositions)
            {
                column /= _tile.width;
                row /= _tile.height;
                width /= _tile.width;
                height /= _tile.height;
            }
            _rect.x = column;
            _rect.y = row;
            _rect.width = width;
            _rect.height = height;
            _data.fillRect(_rect, solid ? 0xFFFFFFFF : 0);
        }
        
        /**
         * Makes the rectangular region of tiles non-solid.
         * @param    column        First column.
         * @param    row            First row.
         * @param    width        Columns to fill.
         * @param    height        Rows to fill.
         */
        public function clearRect(column:uint = 0, row:uint = 0, width:int = 1, height:int = 1):void
        {
            setRect(column, row, width, height, false);
        }
        
        /**
        * Loads the grid data from a string.
        * @param str            The string data, which is a set of tile values (0 or 1) 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]) > 0);
                }
            }
        }
        
        /**
        * Saves the grid 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;
        }
        
        /**
         * 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 grid has
         */
        public function get columns():uint { return _columns; }
        
        /**
         * How many rows the grid has.
         */
        public function get rows():uint { return _rows; }
        
        /**
         * The grid data.
         */
        public function get data():BitmapData { return _data; }
        
        /** @private Collides against an Entity. */
        private function collideMask(other:Mask):Boolean
        {
            _rect.x = other.parent.x - other.parent.originX - parent.x + parent.originX;
            _rect.y = other.parent.y - other.parent.originY - parent.y + parent.originY;
            _point.x = int((_rect.x + other.parent.width - 1) / _tile.width) + 1;
            _point.y = int((_rect.y + other.parent.height -1) / _tile.height) + 1;
            _rect.x = int(_rect.x / _tile.width);
            _rect.y = int(_rect.y / _tile.height);
            _rect.width = _point.x - _rect.x;
            _rect.height = _point.y - _rect.y;
            return _data.hitTest(FP.zero, 1, _rect);
        }
        
        /** @private Collides against a Hitbox. */
        private function collideHitbox(other:Hitbox):Boolean
        {
            _rect.x = other.parent.x + other._x - parent.x - _x;
            _rect.y = other.parent.y + other._y - parent.y - _y;
            _point.x = int((_rect.x + other._width - 1) / _tile.width) + 1;
            _point.y = int((_rect.y + other._height -1) / _tile.height) + 1;
            _rect.x = int(_rect.x / _tile.width);
            _rect.y = int(_rect.y / _tile.height);
            _rect.width = _point.x - _rect.x;
            _rect.height = _point.y - _rect.y;
            return _data.hitTest(FP.zero, 1, _rect);
        }
        
        /** @private Collides against a Pixelmask. */
        private function collidePixelmask(other:Pixelmask):Boolean
        {
            var x1:int = other.parent.x + other._x - parent.x - _x,
                y1:int = other.parent.y + other._y - parent.y - _y,
                x2:int = ((x1 + other._width - 1) / _tile.width),
                y2:int = ((y1 + other._height - 1) / _tile.height);
            _point.x = x1;
            _point.y = y1;
            x1 /= _tile.width;
            y1 /= _tile.height;
            _tile.x = x1 * _tile.width;
            _tile.y = y1 * _tile.height;
            var xx:int = x1;
            while (y1 <= y2)
            {
                while (x1 <= x2)
                {
                    if (_data.getPixel32(x1, y1))
                    {
                        if (other._data.hitTest(_point, 1, _tile)) return true;
                    }
                    x1 ++;
                    _tile.x += _tile.width;
                }
                x1 = xx;
                y1 ++;
                _tile.x = x1 * _tile.width;
                _tile.y += _tile.height;
            }
            return false;
        }
        
        // Grid information.
        /** @private */ private var _data:BitmapData;
        /** @private */ private var _columns:uint;
        /** @private */ private var _rows:uint;
        /** @private */ private var _tile:Rectangle;
        /** @private */ private var _rect:Rectangle = FP.rect;
        /** @private */ private var _point:Point = FP.point;
        /** @private */ private var _point2:Point = FP.point2;
    }
}