package flare.vis.operator.layout
{
    import flare.util.Property;
    import flare.vis.data.Data;
    import flare.vis.data.DataList;
    import flare.vis.data.NodeSprite;
    import flare.vis.data.ScaleBinding;
    
    import flash.geom.Point;
    import flash.geom.Rectangle;
    
    /**
     * Layout that places items in a circular layout. This operator is quite
     * flexible and offers a number of layout options:
     * <ul>
     *  <li>By default, all items are arranged along the circumference of the
     *      circle using the sort order of the underlying data list.</li>
     *  <li>If a data field for either the radius or angle is provided, this
     *      layout will act as a radial scatter plot, using the data fields
     *      to determine the radius or angle values of the layout.</li>
     *  <li>If no data field is provided but the <code>treeLayout</code>
     *      property is set to <code>true</code>, the layout will use an
     *      underlying tree structure to layout the data. Leaf nodes will be
     *      placed along the circumference of the circle, but parent nodes will
     *      be placed in the interior. Also, the layout will add spacing to
     *      differentiate sibling groups along the circumference.</li>
     * </ul>
     * 
     * <p>The layout also supports mixes of the above modes. For example, if
     * <code>treeLayout</code> is set to <code>true</code> and a data field for
     * the radius is set, the angles in the layout will be determined as in
     * a normal ciruclar tree layout, but the radius values will be derived
     * using the data field.</p>
     */
    public class CircleLayout extends Layout
    {    
        /** The padding around the circumference of the circle, in pixels. */
        public var padding:Number = 50;
        /** The starting angle for the layout, in radians. */
        public var startAngle:Number = Math.PI / 2;
        /** The angular width of the layout, in radians (default is 2 pi). */
        public var angleWidth:Number = 2 * Math.PI;
        /** Flag indicating if tree structure should inform the layout. */
        public var treeLayout:Boolean = false;
        
        protected var _inner:Number = 0, _innerFrac:Number = NaN;
        protected var _outer:Number;
        protected var _group:String;
        protected var _rField:Property;
        protected var _aField:Property;
        protected var _rBinding:ScaleBinding;
        protected var _aBinding:ScaleBinding;
        
        /** The starting (inner) radius at which to place items. 
         *  Setting this value also overrides the
         *  <code>startRadiusFraction</code> property. */
        public function get startRadius():Number { return _inner; }
        public function set startRadius(r:Number):void {
            _inner = r; _innerFrac = NaN;
        }
        
        /** The starting (inner) radius as a fraction of the outer radius. 
         *  Setting this value also overrides the 
         *  <code>startRadius</code> property. When this property is set to
         *  <code>NaN</code>, the current value of <code>startRadius</code>
         *  will be used directly. */
        public function get startRadiusFraction():Number { return _innerFrac; }
        public function set startRadiusFraction(f:Number):void {
            _innerFrac = f;
        }
        
        /** The radius source property. */
        public function get radiusField():String { return _rBinding.property; }
        public function set radiusField(f:String):void { _rBinding.property = f; }
        
        /** The angle source property. */
        public function get angleField():String { return _aBinding.property; }
        public function set angleField(f:String):void { _aBinding.property = f; }
        
        /** The scale binding for the radius. */
        public function get radiusScale():ScaleBinding { return _rBinding; }
        public function set radiusScale(b:ScaleBinding):void {
            if (_rBinding) {
                if (!b.property) b.property = _rBinding.property;
                if (!b.group) b.group = _rBinding.group;
                if (!b.data) b.data = _rBinding.data;
            }
            _rBinding = b;
        }
        
        /** The scale binding for the angle. */
        public function get angleScale():ScaleBinding { return _aBinding; }
        public function set angleScale(b:ScaleBinding):void {
            if (_aBinding) {
                if (!b.property) b.property = _aBinding.property;
                if (!b.group) b.group = _aBinding.group;
                if (!b.data) b.data = _aBinding.data;
            }
            _aBinding = b;
        }
        
        // --------------------------------------------------------------------
                
        /**
         * Creates a new CircleLayout.
         * @param radiusField optional data field to encode as radius length
         * @param angleField optional data field to encode as angle
         * @param treeLayout boolean flag indicating if any tree-structure in
         *  the data should be used to inform the layout
         * @param group the data group to process. If tree layout is set to
         *  true, this value may get ignored.
         */
        public function CircleLayout(
            radiusField:String=null, angleField:String=null,
            treeLayout:Boolean=false, group:String=Data.NODES)
        {
            layoutType = POLAR;
            _group = group;
            this.treeLayout = treeLayout;
            
            _rBinding = new ScaleBinding();
            _rBinding.group = _group;
            _rBinding.property = radiusField;
            
            _aBinding = new ScaleBinding();
            _aBinding.group = _group;
            _aBinding.property = angleField;
        }
        
        /** @inheritDoc */
        public override function setup():void
        {
            if (visualization==null) return;
            _rBinding.data = visualization.data;
            _aBinding.data = visualization.data;
        }
        
        /** @inheritDoc */
        protected override function layout():void
        {            
            var list:DataList = visualization.data.group(_group);
            var i:int = 0, N:int = list.length, dr:Number;
            var visitor:Function = null;
            
            // determine radius
            var b:Rectangle = layoutBounds;
            _outer = Math.min(b.width, b.height)/2 - padding;
            _inner = isNaN(_innerFrac) ? _inner : _outer * _innerFrac;
            
            // set the anchor point
            var anchor:Point = layoutAnchor;
            list.visit(function(n:NodeSprite):void { n.origin = anchor; });
            
            // compute angles
            if (_aBinding.property) {
                // if angle property, get scale binding and do layout
                _aBinding.updateBinding();
                _aField = Property.$(_aBinding.property);
                visitor = function(n:NodeSprite):void {
                    var f:Number = _aBinding.interpolate(_aField.getValue(n));
                    _t.$(n).angle = minAngle(n.angle, 
                                             startAngle - f*angleWidth);
                };
            } else if (treeLayout) {
                // if tree mode, use tree order
                setTreeAngles();
            } else {
                // if nothing use total sort order
                i = 0;
                visitor = function(n:NodeSprite):void {
                    _t.$(n).angle = minAngle(n.angle,
                                             startAngle - (i/N)*angleWidth);
                    i++;
                };
            }
            if (visitor != null) list.visit(visitor);
            
            // compute radii
            visitor = null;
            if (_rBinding.property) {
                // if radius property, get scale binding and do layout
                _rBinding.updateBinding();
                _rField = Property.$(_rBinding.property);
                dr = _outer - _inner;
                visitor = function(n:NodeSprite):void {
                    var f:Number = _rBinding.interpolate(_rField.getValue(n));
                    _t.$(n).radius = _inner + f * dr;
                };
            } else if (treeLayout) {
                // if tree-mode, use tree depth
                setTreeRadii();
            } else {
                // if nothing, use outer radius
                visitor = function(n:NodeSprite):void {
                    _t.$(n).radius = _outer;
                };
            }
            if (visitor != null) list.visit(visitor);
            if (treeLayout) _t.$(visualization.data.tree.root).radius = 0;
            
            // finish up
            updateEdgePoints(_t);
        }
        
        private function setTreeAngles():void
        {
            // first pass, determine the angular spacing
            var root:NodeSprite = visualization.tree.root, p:NodeSprite = null;
            var leafCount:int = 0, parentCount:int = 0;
            root.visitTreeDepthFirst(function(n:NodeSprite):void {
                if (n.childDegree == 0) {
                    if (p != n.parentNode) {
                        p = n.parentNode;
                        ++parentCount;
                    }
                    ++leafCount;
                }
            });
            var inc:Number = (-angleWidth) / (leafCount + parentCount);
            var angle:Number = startAngle;
            
            // second pass, set the angles
            root.visitTreeDepthFirst(function(n:NodeSprite):void {
                var a:Number = 0, b:Number;
                if (n.childDegree == 0) {
                    if (p != n.parentNode) {
                        p = n.parentNode;
                        angle += inc;
                    }
                    a = angle;
                    angle += inc;
                } else if (n.parent != null) {
                    a = _t.$(n.firstChildNode).angle;
                    b = _t.$(n.lastChildNode).angle - a;
                    while (b >  Math.PI) b -= 2*Math.PI;
                    while (b < -Math.PI) b += 2*Math.PI;
                    a += b / 2;
                }
                _t.$(n).angle = minAngle(n.angle, a);
            });
        }
        
        private function setTreeRadii():void
        {
            var n:NodeSprite;
            var depth:Number = 0, dr:Number = _outer - _inner;
            
            for each (n in visualization.tree.nodes) {
                if (n.childDegree == 0) {
                    depth = Math.max(n.depth, depth);
                    _t.$(n).radius = _outer;
                }
            }
            for each (n in visualization.tree.nodes) {
                if (n.childDegree != 0) {
                    _t.$(n).radius = _inner + (n.depth/depth) * dr;
                }
            }
            
            n = visualization.tree.root;
            if (!_t.immediate) {
                delete _t._(n).values.radius;
                delete _t._(n).values.angle;
            }
            _t.$(n).x = n.origin.x;
            _t.$(n).y = n.origin.y;
        }
        
    } // end of class CircleLayout
}