package com.yourmajesty.effects.distortion
{
    import flash.display.*;
    import flash.filters.DisplacementMapFilter;
    import flash.filters.DisplacementMapFilterMode;
    import flash.geom.Matrix;
    import flash.geom.Point;
    import flash.geom.Rectangle;
    import flash.utils.ByteArray;
    
    /**
     * Distorts any IBitmapDrawable display object so that it appears as if it were mapped on the front
     * of a cylinder. Using this class you may rotate the cylinder around its primary axis, modify the amount
     * that the image is compressed on the edges, and how much it arcs (up or down).
     * To use the class, simply create one, attach a display object to it, and add its "wet" property to your
     * display list. The wet image is the post-processed image.
     * 
     * If you use this class, please retain the following author tag:
     * @author Roger Braunstein | Your Majesty Co | 2007
     */
    public class CylinderMap implements IDistortionMap
    {
        protected static const ORIGIN:Point = new Point();
        protected static const X_CHANNEL:uint = BitmapDataChannel.BLUE;
        protected static const Y_CHANNEL:uint = BitmapDataChannel.RED;

        protected var target:IBitmapDrawable;
        protected var targetProjection:Matrix;
        protected var map:BitmapData;
        protected var mapSprite:Sprite;
        protected var mapGraphics:Graphics;
        protected var surface:BitmapData;
        protected var surfaceBitmap:Bitmap;
        protected var filter:DisplacementMapFilter;
        
        public function get wet():Bitmap {return surfaceBitmap;}
        
        /** height of the arc expressed as a percentage of the width, so that 1 should represent a semicircle. Negative numbers are acceptable */
        public function set arc(value:Number):void {_arc = value; updateMap();}
        public function get arc():Number {return _arc;}
        protected var _arc:Number = 0.2;
        
        /** amount of perspective applied to the edges of the cylinder as you look straight on it */
        public function set sideCompression(value:Number):void {_sideCompression = value; updateMap();}
        public function get sideCompression():Number {return _sideCompression;}
        protected var _sideCompression:Number = 50;
        
        /** allows you to simulate the cylinder rotating around. Expressed in degrees. Clockwise. From off the surface left = 0 to off the surface right = 360. */
        public function set rotation(value:Number):void { _rotation = value; updateRotation();}
        public function get rotation():Number {return _rotation;}
        protected var _rotation:Number = 0;
        
        protected const NEUTRAL_DISPLACE:int = 128; //starting with a neutral displacement lets us shift things negatively as well as positively.
        
        public function CylinderMap(width:Number = NaN, height:Number = NaN, mc:IBitmapDrawable = null)
        {
            if (mc) attachDisplayObject(mc);
            if (!isNaN(width) && !isNaN(height)) createMap(width, height);
        }
        
        public function destroy():void
        {
            if (surface) surface.dispose();
            if (map) map.dispose();
            //target = map = surface = surfaceBitmap = filter = null;
        }

        public function attachDisplayObject(mc:IBitmapDrawable):void
        {
            target = mc;
            targetProjection = new Matrix();
        }
        
        public function createMap(width:Number, height:Number):void
        {
            //TODO: clear old maps if this is called twice
            map = new BitmapData(int(width), int(height), true, 0);
            targetProjection.ty = int(0.5 * (map.height - DisplayObject(target).height));
            mapSprite = new Sprite();
            mapGraphics = mapSprite.graphics;
            surface = new BitmapData(int(width), int(height), true, 0);
            surfaceBitmap = new Bitmap(surface);
            filter = new DisplacementMapFilter(map, ORIGIN, X_CHANNEL, Y_CHANNEL, 0, 0, DisplacementMapFilterMode.COLOR, 0, 0);
            
            initializeMap();
            
            updateMap();
        }
        
        protected function initializeMap():void
        {
            
            var xColors:Array = new Array(map.width);
            var yColors:Array = new Array(map.height);
            
            var horizPixelStrip:ByteArray = new ByteArray();

            var scale:Number = 2 / Math.PI;
            var x:int, y:int, limit:int, value:Number;
            var redValue:uint, blueValue:uint, redX:Number, blueX:Number, redDX:Number, blueDX:Number;
            var X:Number, gap:Number;
            
            //blue = horizontal compression, arcsin from -1 to 1
            //red = vertical arching, sqrt(1-x^2)
            for (x = 0, limit = map.width,
                 blueX = -1, blueDX = 2 / map.width,
                 redX = -1, redDX = 2 / map.width;
                 
                 x < limit; x++, redX += redDX, blueX += blueDX)
            {
                value = scale * Math.asin( blueX ) * Math.pow(blueX, 6); //goes from -1 to 1 like a sideways sin
                value = NEUTRAL_DISPLACE + NEUTRAL_DISPLACE * value; //now it goes from 0 to 255
                blueValue = (int(value) & 0xff);
                
                value = Math.sqrt( 1 - redX*redX ); //goes from 0 to 1 to 0
                value = NEUTRAL_DISPLACE - 1 + NEUTRAL_DISPLACE * value; //now it goes from 128 to 255
                redValue = (int(value) & 0xff);
                
                horizPixelStrip.writeUnsignedInt(0xff000000 + (redValue << 16) + blueValue);
            }
            
            var r:Rectangle = new Rectangle(0, 0, map.width, 1);
            for (y = 0, limit = map.height; y < limit; y++, r.y++)
            {
                horizPixelStrip.position = 0;
                map.setPixels(r, horizPixelStrip);
            }
        }
        
        protected function updateRotation():void
        {
            if (!targetProjection) return;
            var value:Number = _rotation % 360;
            var w:Number = DisplayObject(target).width;
            targetProjection.tx = (map.width + w) * (value / 360) - w;
        }
        
        public function updateMap():void
        {
            filter.scaleY = _arc * map.width;
            filter.scaleX = _sideCompression;
        }
        
        public function draw():void
        {
            surface.fillRect(surface.rect, 0);
            surface.draw(target, targetProjection);
            surface.applyFilter(surface, surface.rect, new Point(0, 0), filter);
        }
        
    }
}