    package flare.vis.legend
    {
        import flare.animate.Transitioner;
        import flare.display.RectSprite;
        import flare.display.TextSprite;
        import flare.scale.Scale;
        import flare.scale.ScaleType;
        import flare.util.Displays;
        import flare.util.Orientation;
        import flare.util.palette.ColorPalette;
        import flare.util.palette.Palette;
        import flare.util.palette.ShapePalette;
        import flare.util.palette.SizePalette;
        import flare.vis.data.ScaleBinding;

        import flash.display.Sprite;
        import flash.geom.Rectangle;
        import flash.text.TextFormat;

        /**
         * A legend describing the visual encoding of a data property. Legends
         * support both discrete legends that list individual items and
         * range legends the convey a continuous range of values. Discrete
         * legends consist of a collection of <code>LegendItem</code> instances
         * stored in the <code>items</code> sprite. Range legends consist of
         * a single <code>LegendRange</code> instance stored in the
         * <code>items</code> sprite.
         *
         * <p>There are multiple ways to generate a legend. To build a legend
         * based on an existing visual encoding, use the static
         * <code>fromScale</code> constructor. This method takes a data scale
         * and one more or more palettes (e.g., color, shape, or size palettes)
         * and uses them to generate an appropriate legend. If the data scale
         * is a quantitative scale and only a color palette is provided, a
         * continuous range legend will be generated. Otherwise, a discrete
         * legend will be created.</p>
         *
         * <p>Legends can also be created from a collection of independent
         * values using the static <code>fromValues</code> constructor. This
         * method takes an array of legend item descriptions and uses them to
         * generate a legend. For example, consider this code:</p>
         *
         * <pre>
         * var legend:Legend = Legend.fromValues("Legend Title", [
         *   {color: 0xff0000, shape:Shapes.X, label:"Red X"},
         *   {color: 0x00ff00, shape:Shapes.SQUARE, label:"Green Square"},
         *   {color: 0x0000ff, shape:Shapes.CIRCLE, label:"Blue Circle"}
         * ]);
         * </pre>
         *
         * <p>This example will create a legend with the described values.
         * See the documentation for the <code>buildFromValues</code> method
         * for more details.</p>
         */
        public class Legend extends Sprite
        {
            /** @private The layout bounds for this legend instance. */
            protected var _bounds:Rectangle = null;

            /** @private Sprite defining the border of the legend. */
            protected var _border:RectSprite;
            /** @private Sprite containing the legend items. */
            protected var _items:Sprite;
            /** @private TextSprite containing the legend title.*/
            protected var _title:TextSprite;

            /** @private Scale instance used to define the legend mapping. */
            protected var _scale:Scale;
            /** @private Flag for if this legend is discrete or continuous. */
            protected var _discrete:Boolean = true;

            /** @private The default color to use for legend items. */
            protected var _defaultColor:uint = 0xff888888;
            /** @private The color palette used to encode values (may be null). */
            protected var _colors:ColorPalette;
            /** @private The shape palette used to encode values (may be null). */
            protected var _shapes:ShapePalette;
            /** @private The size palette used to encode values (may be null). */
            protected var _sizes:SizePalette;

            /** @private Flag indicating the desired orientation of this legend. */
            protected var _orient:String = null;
            /** @private Margins within legend items. */
            protected var _margin:Number = 4;
            /** @private Spacing between legend items. */
            protected var _spacing:Number = 0;
            /** @private Base icon size */
            protected var _baseIconSize:Number = 12;

            /** @private TextFormat (font, size, style) of legend item labels. */
            protected var _labelTextFormat:TextFormat = new TextFormat("Arial",12,0);
            /** @private Label text mode. */
            protected var _labelTextMode:int = TextSprite.BITMAP;

            /** @private The calculated internal width of the legend. */
            protected var _iw:Number;
            /** @private The calculated internal height of the legend. */
            protected var _ih:Number;

            // -- Properties ------------------------------------------------------

            /** The layout bounds for this legend instance. */
            public function get bounds():Rectangle { return _bounds; }
            public function set bounds(b:Rectangle):void { _bounds = b; }

            /** Sprite defining the border of the legend. */
            public function get border():RectSprite { return _border; }
            /** Sprite containing the legend items. */
            public function get items():Sprite { return _items; }
            /** TextSprite containing the legend title.*/
            public function get title():TextSprite { return _title; }

            /** Flag indicating if this legend is discrete or continuous. */
            public function get discrete():Boolean { return _discrete; }

            /** Scale instance used to define the legend mapping. */
            public function get scale():Scale { return _scale; }
            public function set scale(s:Scale):void { _scale = s; }
            /** The legend range, if this legend is continuous. This
             *  value is null if the legend is discrete. */
            public function get range():LegendRange {
                return _discrete ? null : LegendRange(_items.getChildAt(0));
            }

            /** The default color to use for legend items. */
            public function get defaultColor():uint { return _defaultColor; }
            public function set defaultColor(c:uint):void { _defaultColor = c; }

            /** The color palette used to encode values (may be null). */
            public function get colorPalette():ColorPalette { return _colors; }
            public function set colorPalette(cp:ColorPalette):void { _colors = cp; }

            /** The shape palette used to encode values (may be null). */
            public function get shapePalette():ShapePalette { return _shapes; }
            public function set shapePalette(sp:ShapePalette):void { _shapes = sp; }

            /** The size palette used to encode values (may be null). */
            public function get sizePalette():SizePalette { return _sizes; }
            public function set sizePalette(sp:SizePalette):void { _sizes = sp; }

            /** The desired orientation of this legend. */
            public function get orientation():String { return _orient; }
            public function set orientation(o:String):void { _orient = o; }

            /** Margins within legend items. */
            public function get margin():Number { return _margin; }
            public function set margin(m:Number):void { _margin = m; }

            /** Spacing between legend items. */
            public function get spacing():Number { return _spacing; }
            public function set spacing(s:Number):void { _spacing = s; }

            /** Base icon size, corresponding to a size factor of 1. */
            public function get baseIconSize():Number { return _baseIconSize; }
            public function set baseIconSize(s:Number):void {
                if (_baseIconSize != s && _discrete) {
                    for (var i:uint=0; i<_items.numChildren; ++i) {
                        var li:LegendItem = LegendItem(_items.getChildAt(i));
                        li.iconSize *= (s / _baseIconSize);
                        li.maxIconSize *= (s / _baseIconSize);
                    }
                }
                _baseIconSize = s;
            }

            /** TextFormat (font, size, style) of legend item labels. */
            public function get labelTextFormat():TextFormat { return _labelTextFormat; }
            public function set labelTextFormat(f:TextFormat):void {
                _labelTextFormat = f; updateItems();
            }

            /** Label text mode. */
            public function get labelTextMode():int { return _labelTextMode; }
            public function set labelTextMode(mode:int):void {
                _labelTextMode = mode; updateItems();
            }

            // -- Initialization --------------------------------------------------

            /**
             * Creates a new Legend for the given data field.
             * @param dataField the data field to describe with the legend
             * @param vis the visualization corresponding to this legend
             * @param scale the scale value used to map the data field to visual
             *  variables
             */
            public function Legend(title:String, scale:Scale=null,
                colors:ColorPalette=null, shapes:ShapePalette=null,
                sizes:SizePalette=null)
            {
                this.scale = scale;
                addChild(_border = new RectSprite(0,0,0,0,13,13));
                addChild(_title = new TextSprite());
                addChild(_items = new Sprite());
                _border.lineColor = 0;
                _border.fillColor = 0;

                _colors = colors;
                _shapes = shapes;
                _sizes = sizes;

                _title.textField.defaultTextFormat =
                    new TextFormat("Arial",12,null,true);
                if (title != null)
                    _title.text = title;
                else
                    _title.visible = false;

                if (scale != null) {
                    buildFromScale();
                    update();
                }
            }

            /**
             * Update the legend, recomputing layout of items.
             * @param t a transitioner for value updates
             * @return the input transitioner
             */
            public function update(t:Transitioner=null):Transitioner
            {
                if (_scale is ScaleBinding && ScaleBinding(_scale).updateBinding())
                    buildFromScale();
                updateItems();
                layout(t);
                return t;
            }

            /**
             * Builds the contents of this legend from the current scale values.
             * This method will remove all items from the legend and rebuild the
             * legend using the current scale and palette settings.
             */
            public function buildFromScale():void
            {
                // first, remove all items
                while (_items.numChildren > 0) {
                    _items.removeChildAt(_items.numChildren-1);
                }

                // determine legend type
                var type:String = _scale.scaleType;
                if (ScaleType.isQuantitative(type) && !_sizes && !_shapes) {
                    // build continuous legend
                    _discrete = false;
                    if (!_orient) _orient = Orientation.LEFT_TO_RIGHT;
                    _items.addChild(new LegendRange(_title.text,
                        _scale,    _colors, _orient));
                } else {
                    // build discrete legend
                    _discrete = true;
                    if (!_orient) _orient = Orientation.TOP_TO_BOTTOM;

                    var numVals:int = ScaleType.isQuantitative(type) ? 5 : -1;
                    var maxSize:Number = Number.MIN_VALUE;
                    var vals:Array = _scale.values(numVals);
                    for (var i:uint=0; i<vals.length; ++i) {
                        // determine legend item properties
                        var f:Number = _scale.interpolate(vals[i]);
                        var color:uint = _defaultColor;
                        if (_colors && ScaleType.isOrdinal(type)) {
                            color = _colors.getColorByIndex(i);
                        } else if (_colors) {
                            color = _colors.getColor(f);
                        }
                        var shape:String = _shapes ? _shapes.getShape(i) : null;
                        var size:Number = _baseIconSize*(_sizes?_sizes.getSize(f):1);
                        if (size > maxSize) maxSize = size;

                        var item:LegendItem = new LegendItem(
                            _scale.label(vals[i]), color, shape, size);
                        item.value = vals[i];
                        _items.addChild(item);
                    }
                    for (i=0; i<_items.numChildren; ++i)
                        LegendItem(_items.getChildAt(i)).maxIconSize = maxSize;
                }
            }

            /**
             * Populates the contents of this legend from a list of value objects.
             * This method will create a legend with discrete entries determined by
             * the contents of the input <code>values</code> array. This should be
             * an array of objects containing the following properties:
             * <ul>
             *  <li><code>value</code>: The data value the legend item represents.
             *    This value is not required.</li>
             *  <li><code>label</code>: The text label to place in the legend item.
             *    If this value is not provided, the method will attempt to
             *    generate a label string from the <code>value</code> property.</li>
             *  <li><code>color</code>: The color for the legend item. If missing,
             *    this legend's default color will be used.</li>
             *  <li><code>shape</code>: The shape for the legend item. If missing,
             *    a default circle shape will be used.</li>
             *  <li><code>size</code>: The size for the legend item. If missing,
             *    a size value of 1 will be used.</li>
             * </ul>
             * When this method is called, any previous values in the legend will
             * be removed.
             * @param values an array of value to include in the legend.
             */
            public function buildFromValues(values:Array):void
            {
                // first, remove all items
                while (_items.numChildren > 0) {
                    _items.removeChildAt(_items.numChildren-1);
                }
                _discrete = true;
                if (!_orient) _orient = Orientation.TOP_TO_BOTTOM;

                var maxSize:Number = Number.MIN_VALUE;
                for each (var v:Object in values) {
                    var value:Object = v.value != undefined ? v.value : null;
                    var label:String = v.label ? v.label.toString() : value ? value.toString() : "???";
                    var color:uint = v.color != undefined ? uint(v.color) : _defaultColor;
                    var shape:String = v.shape ? v.shape as String : null;
                    var size:Number = _baseIconSize*(v.size is Number ? v.size : 1);
                    if (size > maxSize) maxSize = size;

                    var item:LegendItem = new LegendItem(label, color, shape, size);
                    item.value = value;
                    item.margin = margin;
                    _items.addChild(item);
                }
                for (var i:uint=0; i<_items.numChildren; ++i)
                    LegendItem(_items.getChildAt(i)).maxIconSize = maxSize;
            }

            // -- Layout ----------------------------------------------------------

            /**
             * Performs layout, setting the position for all items in the legend.
             * @param t a transitioner for value updates
             */
            public function layout(t:Transitioner=null):void
            {
                t = (t ? t : Transitioner.DEFAULT);

                var vert:Boolean = Orientation.isVertical(_orient);
                var o:Object;
                var b:Rectangle = bounds;
                var x:Number = b ? b.left : 0;
                var y:Number = b ? b.top : 0;
                var w:Number, h:Number, th:Number = 0;

                // layout the title
                o = t.$(_title);
                if (_title.text != null && _title.text.length > 0) {
                    o.x = x + _margin;
                    o.alpha = 1;
                    _title.visible = true;
                    y += (th = _title.height + (vert?_spacing:0));
                } else {
                    o.alpha = 0;
                    o.visible = false;
                }

                // layout item container
                o = t.$(_items);
                o.x = x;
                o.y = y;

                // layout items
                if (_discrete) {
                    layoutDiscrete(t);
                } else {
                    layoutContinuous(t);
                }

                w = b ? (vert ? b.width : Math.min(_iw, b.width)) : _iw;
                h = b ? (vert ? Math.min(_ih, b.height) : _ih) : _ih;

                // size the border
                o = t.$(_border);
                o.x = x;
                o.y = b ? b.top : 0;
                o.w = w;
                o.h = h + th;
                if (t.immediate) _border.render();

                // create clipping panel
                t.$(items).scrollRect = new Rectangle(0, 0, 1+w, 1+h);
            }

            /**
             * @private
             * Layout helper for positioning discrete legend items.
             * @param t a transitioner for value updates
             */
            protected function layoutDiscrete(t:Transitioner):void
            {
                var vert:Boolean = Orientation.isVertical(_orient);
                var rev:Boolean = _orient == Orientation.RIGHT_TO_LEFT ||
                                  _orient == Orientation.BOTTOM_TO_TOP;
                var x:Number = 0, y:Number = 0, i:uint, j:uint;
                var item:LegendItem, o:Object;
                var bw:Number = vert && bounds ? bounds.width : NaN;

                // if needed, compute shared width for legend items
                if (vert && isNaN(bw)) {
                    bw = Number.MIN_VALUE;
                    for (i=0; i<_items.numChildren; ++i) {
                        item = _items.getChildAt(i) as LegendItem;
                        bw = Math.max(bw, item.innerWidth);
                    }
                }

                _iw = _ih = 0;
                for (i=0; i<_items.numChildren; ++i) {
                    j = rev ? _items.numChildren-i-1 : i;
                    // layout the item
                    item = _items.getChildAt(j) as LegendItem;
                    o = t.$(item);
                    o.x = x;
                    o.y = y;
                    o.w = isNaN(bw) ? item.innerWidth : bw;

                    // increment spacing
                    if (vert) {
                        y += item.innerHeight + _spacing;
                        _iw = Math.max(_iw, item.innerWidth);
                    }
                    else {
                        x += item.innerWidth + _spacing;
                        _ih = Math.max(_ih, item.innerHeight);
                    }
                }
                _iw = vert ? _iw : x-_spacing;
                _ih = vert ? y-_spacing : _ih;
            }

            /**
             * @private
             * Layout helper for positioning a continous legend range.
             * @param trans a transitioner for value updates
             */
            protected function layoutContinuous(t:Transitioner):void
            {
                var lr:LegendRange = _items.getChildAt(0) as LegendRange;
                lr.orientation = _orient;
                if (Orientation.isHorizontal(_orient)) {
                    _iw = lr.w = bounds ? bounds.width : 200;
                    lr.updateLabels();
                    _ih = lr.height + lr.margin;
                } else {
                    _ih = lr.h = bounds ? bounds.height : 200;
                    lr.updateLabels();
                    _iw = lr.width;
                }
            }

            // -- Legend Items ----------------------------------------------------

            /** @private */
            protected function updateItems() : void
            {
                if (_items.numChildren == 0) {
                    return;
                } else if (_discrete) {
                    for (var i:uint = 0; i<_items.numChildren; ++i)
                        updateItem(_items.getChildAt(i) as LegendItem);
                } else {
                    updateRange(_items.getChildAt(0) as LegendRange);
                }
            }

            /** @private */
            protected function updateItem(item:LegendItem):void
            {
                item.label.textMode = _labelTextMode;
                item.label.applyFormat(_labelTextFormat);
                item.margin = _margin;
            }

            /** @private */
            protected function updateRange(range:LegendRange):void
            {
                range.labelTextMode = _labelTextMode;
                range.labelTextFormat = _labelTextFormat;
                range.margin = _margin;
            }

            /**
             * Sets property values on all legend items. The values
             * within the <code>vals</code> argument can take a number of forms:
             * <ul>
             *  <li>If a value is a <code>Function</code>, it will be evaluated
             *      for each element and the result will be used as the property
             *      value for that element.</li>
             *  <li>If a value is an <code>IEvaluable</code> instance, such as
             *      <code>flare.util.Property</code> or
             *      <code>flare.query.Expression</code>, it will be evaluated for
             *      each element and the result will be used as the property value
             *      for that element.</li>
             *  <li>In all other cases, a property value will be treated as a
             *      literal and assigned for all elements.</li>
             * </ul>
             * @param vals an object containing the properties and values to set.
             * @param t a transitioner or time span for updating object values. If
             *  the input is a transitioner, it will be used to store the updated
             *  values. If the input is a number, a new Transitioner with duration
             *  set to the input value will be used. The input is null by default,
             *  in which case object values are updated immediately.
             * @return the transitioner used to update the values
             */
            public function setItemProperties(vals:Object, t:*=n