package {
    [SWF(width="384", heigh="256", frameRate="20") ]
    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.ui.Keyboard;
    import flash.utils.ByteArray;
    import flash.utils.getTimer;
    import flash.text.TextField;

    public class Fractal extends Sprite
    {
        // image classes
        private var data:BitmapData;
        private var image:Bitmap;
        // a cache of computed pixels
        private var array:ByteArray;
        // what complex area are we showing
        private var xMin:Number = -2;
        private var xMax:Number = .5;
        private var yMin:Number = -1;
        private var yMax:Number = 1;
        // are we moving, and how
        private var isMoving:Boolean = false;
        private var isZoom:Boolean = true;
        // color mapping
        private var colorMap:Array;
        // rendering area and quality.
        private const gridWidth:uint = 384;
        private const gridHeight:uint = 256;
        private const gridScaleX:uint = 1;
        private const gridScaleY:uint = 1;
        private const maxChunkSize:uint = 64;
        private const maxiter:uint = 255;
        // debug stuff
/*        
        private var t1:TextField;
        private var t2:TextField;
        private var t3:TextField;
        private var t4:TextField;
*/        
        
        public function Fractal()
        {
            stage.scaleMode = "noScale";
            createColorMap();
            
            var holder:Sprite = new Sprite;
            data = new BitmapData(gridWidth, gridHeight, false, 0x000000);
            image = new Bitmap(data, "auto", true);
            array  = new ByteArray();
            scaleX = gridScaleX;
            scaleY = gridScaleY;
            holder.addChild(image);
            
            holder.addEventListener(MouseEvent.MOUSE_DOWN, startMoving);
            holder.addEventListener(MouseEvent.MOUSE_UP, stopMoving);
            holder.addEventListener(Event.ENTER_FRAME, move);
            stage.addEventListener(KeyboardEvent.KEY_DOWN, zoomMode);
            stage.addEventListener(KeyboardEvent.KEY_UP, zoomMode);
            addChild(holder);

            // add some debug fields.
/*            
            t1 = new TextField;
            t2 = new TextField;
            t3 = new TextField;
            t4 = new TextField;
            addChild(t1);
            addChild(t2);
            addChild(t3);
            addChild(t4);
            t1.border = t2.border = t3.border = t4.border = true;
            t1.background = t2.background = t3.background = t4.background = true;
            t1.x = t2.x = t3.x = t4.x = 400;
            t1.y = 0;
            t2.y = 30;
            t3.y = 60;
            t4.y = 90;
            t1.width = t2.width = t3.width = t4.width = 170;
            t1.height = t2.height = t3.height = t4.height = 20;
*/
            // start with a high-res rendering.
            draw();
            
        }
        
        public final function startMoving(event:MouseEvent):void {
            isMoving = true;
        }
        public final function stopMoving(event:MouseEvent):void {
            isMoving = false;
            // do a high-res rendering.
            draw();
        }
        public final function zoomMode(event:KeyboardEvent):void {
            if (event.keyCode == Keyboard.CONTROL) {
                isZoom = (event.type == KeyboardEvent.KEY_UP);
            }
        }
        
        public final function move(event:Event):void {
            if (isMoving) {
                if (isZoom) {
                    zoom();
                } else {
                    unzoom();
                }
            }
        }

        public final function zoom():void {
            var mx:int = image.mouseX;
            var my:int = image.mouseY;
            var xCenter:Number = xMin + (xMax - xMin) * mx / gridWidth;
            var yCenter:Number = yMin + (yMax - yMin) * my / gridHeight;
            xMin += (xCenter-xMin)/16;
            xMax += (xCenter-xMax)/16;
            yMin += (yCenter-yMin)/16;
            yMax += (yCenter-yMax)/16;
            if ((xMax - xMin < 1e-12)||(yMax - yMin < 1e-12)) {
                return unzoom();
            }

            draw(true);
        }
        
        public final function unzoom():void {
            var mx:int = image.mouseX;
            var my:int = image.mouseY;
            var xCenter:Number = xMin + (xMax - xMin) * (gridWidth-mx) / gridWidth;
            var yCenter:Number = yMin + (yMax - yMin) * (gridHeight-my) / gridHeight;
            xMin -= (xCenter-xMin)/16;
            xMax -= (xCenter-xMax)/16;
            yMin -= (yCenter-yMin)/16;
            yMax -= (yCenter-yMax)/16;
            if ((xMax - xMin > 4)||(yMax - yMin > 4)) {
                return zoom();
            }
            draw(true);
        }
        
        private final function outOfTime(fast:Boolean, start:int):Boolean {
            if (!fast) return false; // if we're not in a rush, we're never out of time.
            var now:int = getTimer();
            return (now-start > 30);
        }
        
        private final function draw(fast:Boolean = false):void {
/*            
            t1.text = ""+xMin;
            t2.text = ""+xMax;
            t3.text = ""+yMin;
            t4.text = ""+yMax;
*/        
            data.lock();
            var color:uint = 0;
            var parent_x:uint;
            var parent_y:uint;
            array.length = 0;
            var startTime:int = getTimer();
            var mask:uint = (2*maxChunkSize)-1;
            for (var chunkSize:uint = maxChunkSize; chunkSize>0&&(!outOfTime(fast, startTime)); chunkSize>>=1) {
                var yInc:Number = (yMax - yMin) * chunkSize / gridHeight;
                var xInc:Number = (xMax - xMin) * chunkSize / gridWidth;
                var x0:Number = xMin;
                var y0:Number = yMin;
                var x:Number = 0;
                var y:Number = 0;
                var chunk2:uint = 2*chunkSize;
                var rect:Rectangle = new Rectangle(0, 0, chunkSize, chunkSize);
                for (var yPixel:uint = 0; yPixel < gridHeight; yPixel+=chunkSize) {
                    x0 = xMin;

                    parent_y = yPixel;
                    if (yPixel & mask) {
                        parent_y -= chunkSize;
                    }
                    const pyg:uint = parent_y * gridWidth;
                    const ypg:uint = yPixel * gridWidth;

                    for (var xPixel:uint = 0; xPixel < gridWidth; xPixel+=chunkSize) {
                        x = x0;
                        y = y0;
                        
                        parent_x = xPixel;
                        if (xPixel & mask) {
                            parent_x -= - chunkSize;
                        }
                        
                        var do_eval:Boolean = true;
                        if (chunkSize==maxChunkSize) {
                            do_eval = true;
                        } else if ( (xPixel == parent_x) && (yPixel == parent_y) ) {
                            do_eval = false;
                        } else if (same_neighbors(parent_x, parent_y, chunk2)) {
                            do_eval = false;
                        }

                        if (do_eval) {
                            color = escapeIteration(x0, y0) %256;
//                            color = distanceEstimator(x0, y0);
                        } else {
                            color = array[pyg + parent_x];
                        }
                        array[ypg + xPixel] = color;
                        rect.y = yPixel;
                        rect.x = xPixel;
                        data.fillRect(rect, colorMap[color]);
                        
                        x0 += xInc;
                    }
                    
                    y0 += yInc;
                }
                mask >>= 1;
            }
            data.unlock();
        }
        
        private final function same_neighbors(x:uint, y:uint, l:uint):Boolean {
            if (y<l) return false;
            if (x<l) return false;
            var c1:uint = array[(y-l)*gridWidth+(x-l)];
            var c2:uint = array[(y)*gridWidth+(x-l)];
            if (c1!=c2) return false;
            var c3:uint = array[(y+l)*gridWidth+(x-l)];
            if (c1!=c3) return false;
            var c4:uint = array[(y-l)*gridWidth+(x)];
            if (c1!=c4) return false;
            var c5:uint = array[(y+l)*gridWidth+(x)];
            if (c1!=c5) return false;
            var c6:uint = array[(y-l)*gridWidth+(x+l)];
            if (c1!=c6) return false;
            var c7:uint = array[(y)*gridWidth+(x+l)];
            if (c1!=c7) return false;
            var c8:uint = array[(y+l)*gridWidth+(x+l)];
            if (c1!=c8) return false;
            return true;
        }
        
        private final function escapeIteration(x:Number, y:Number):uint {
            var x0:Number = x;
            var y0:Number = y;
            var y2:Number = y*y;
            var x2:Number = x*x;
            var iter:uint = 0;
            while ( (x2+y2 < 4) && (iter < maxiter) ) {
                y = 2*x*y + y0;
                x = x2 - y2 + x0;
                
                x2 = x*x;
                y2 = y*y;
                
                ++iter;
            }
            return iter;
        }
        
        
        
        /**
         * That was supposed to make things prettier.
         * but it don't work. at all. :(
         */
/*         
        private final function distanceEstimator(x:Number, y:Number):uint {
            var x0:Number = x;
            var y0:Number = y;
            var x2:Number = x*x;
            var y2:Number = y*y;
            var dx:Number = 0;
            var dy:Number = 0;
            var iter:uint = 0;
            
            while ( (x2+y2 < 4) && (iter < maxiter) ) {
                x = 2*x*y + y0;
                y = x2 - y2 + x0;

                var t:Number = dx;
                dx = (2* (x*t - y*dy)) + 1;
                dy = (2* (y*t + x*dy)) + 1;
                
                x2 = x*x;
                y2 = y*y;
                
                ++ iter;
            }
            var mz:Number = Math.sqrt(x2 + y2);
            var mdz:Number = Math.sqrt(dx*dx + dy*dy);

            return 200*Math.sqrt(2*Math.log(mz) * mz/mdz);
        }
*/

        private final function mapColor(val:uint):uint {
            var g:uint = val;
            var b:uint = 256 - val
            var r:uint = ((val>>4)&15) | ((val << 4)&240)
            return (r<<16)|(g<<8)|b;
        }
        
        private final function createColorMap():void {
            colorMap = [];
            for (var i:int =0;i<256;i++) {
                colorMap[i] = mapColor(i);
            }
        }
    }
}