    package flare.vis.data
    {
        import flare.scale.LinearScale;
        import flare.scale.LogScale;
        import flare.scale.OrdinalScale;
        import flare.scale.QuantileScale;
        import flare.scale.QuantitativeScale;
        import flare.scale.RootScale;
        import flare.scale.Scale;
        import flare.scale.ScaleType;
        import flare.scale.TimeScale;
        import flare.util.Stats;
        import flare.vis.events.DataEvent;

        /**
         * Utility class that binds a data property to a descriptive scale.
         * A ScaleBinding provides a layer of indirection between a data field and
         * a data scale describing that field. The created scale can be used for
         * layout and encoding of data values. When scale parameters such as the
         * scale type or value range are updated, an underlying scale instance will
         * be updated accordingly or a new instance will be created as needed.
         */
        public class ScaleBinding extends Scale
        {
            /** @private */
            protected var _scale:Scale = null;
            /** @private */
            protected var _scaleType:String = null;
            /** @private */
            protected var _pmin:Object = null;
            /** @private */
            protected var _pmax:Object = null;
            /** @private */
            protected var _base:Number = 10;
            /** @private */
            protected var _bins:int = 5;
            /** @private */
            protected var _power:Number = NaN;
            /** @private */
            protected var _zeroBased:Boolean = false;
            /** @private */
            protected var _ordinals:Array = null;

            /** @private */
            protected var _property:String;
            /** @private */
            protected var _group:String;
            /** @private */
            protected var _data:Data;
            /** @private */
            protected var _stats:Stats;

            /** If true, updates to the underlying data will be ignored, as will
             *  any calls to <code>updateBinding</code>. Set this flag if you want
             *  to prevent the scale values from changing automatically. */
            public var ignoreUpdates:Boolean = false;

            /** The type of scale to create. */
            public override function get scaleType():String {
                return _scaleType ? _scaleType : scale.scaleType;
            }
            public function set scaleType(type:String):void {
                _scaleType = type;
                _scale = null;
            }

            /** The preferred minimum data value for the scale. If null, the scale
             *  minimum will be determined from the data directly. */
            public function get preferredMin():Object { return _pmin; }
            public function set preferredMin(val:Object):void {
                _pmin = val;
                if (_scale && _pmin) {
                    _scale.min = _pmin;
                    if (_zeroBased) zeroAlignScale(_scale);
                }
            }

            /** The preferred maximum data value for the scale. If null, the scale
             *  maximum will be determined from the data directly. */
            public function get preferredMax():Object { return _pmax; }
            public function set preferredMax(val:Object):void {
                _pmax = val;
                if (_scale && _pmax) {
                    _scale.max = _pmax;
                    if (_zeroBased) zeroAlignScale(_scale);
                }
            }

            /** @inheritDoc */
            public override function get max():Object { return scale.max; }
            public override function set max(v:Object):void { scale.max = v; }

            /** @inheritDoc */
            public override function get min():Object { return scale.min; }
            public override function set min(v:Object):void { scale.min = v; }

            /** The number base to use for a quantitative scale (10 by default). */
            public function get base():Number { return _base; }
            public function set base(val:Number):void {
                _base = val;
                if (_scale is QuantitativeScale) {
                    QuantitativeScale(_scale).base = _base;
                }
            }

            /** A free parameter that indicates the exponent for a RootScale. */
            public function get power():Number { return _power; }
            public function set power(val:Number):void {
                _power = val;
                if (_scale is RootScale) {
                    RootScale(_scale).power = _power;
                }
            }

            /** The number of bins for quantile scales. */
            public function get bins():int { return _bins; }
            public function set bins(count:int):void {
                _bins = count;
                if (_scale is QuantileScale) {
                    _scale = null;
                }
            }

            /** Flag indicating if the scale bounds should be flush with the data.
             *  @see flare.scale.Scale#flush */
            public override function get flush():Boolean { return _flush; }
            public override function set flush(val:Boolean):void {
                _flush = val;
                if (_scale) _scale.flush = _flush;
            }

            /** Formatting pattern for formatting labels for scale values.
             *  @see flare.vis.scale.Scale#labelFormat. */
            public override function get labelFormat():String { return _format; }
            public override function set labelFormat(fmt:String):void {
                _format = fmt;
                if (_scale) _scale.labelFormat = fmt;
            }

            /** Flag indicating if a zero-based scale should be used. If set to
             *  true, and the scale type is numerical, the minimum or maximum
             *  scale value will automatically be adjusted to include the zero
             *  point as necessary. */
            public function get zeroBased():Boolean { return _zeroBased; }
            public function set zeroBased(val:Boolean):void {
                _zeroBased = val;
                if (_scale) zeroAlignScale(_scale);
            }

            /** An ordered array of values for defining an ordinal scale. */
            public function get ordinals():Array { return _ordinals; }
            public function set ordinals(ord:Array):void {
                _ordinals = ord;
                if (ScaleType.isOrdinal(_scaleType)) {
                    _stats = null;
                    _scale = null;
                }
            }

            // -----------------------------------------------------

            /** The data instance to bind to. */
            public function get data():Data { return _data; }
            public function set data(data:Data):void {
                if (_data != null) { // remove existing listeners
                    _data.removeEventListener(DataEvent.ADD,   onDataEvent);
                    _data.removeEventListener(DataEvent.REMOVE, onDataEvent);
                    _data.removeEventListener(DataEvent.UPDATE, onDataEvent);
                }
                _data = data;
                if (_data != null) { // add new listeners
                    _data.addEventListener(DataEvent.ADD,
                        onDataEvent, false, 0, true);
                    _data.addEventListener(DataEvent.REMOVE,
                        onDataEvent, false, 0, true);
                    _data.addEventListener(DataEvent.UPDATE,
                        onDataEvent, false, 0, true);
                }
            }

            /** The data group to bind to. */
            public function get group():String { return _group; }
            public function set group(name:String):void {
                if (name != _group) {
                    _group = name;
                    _stats = null;
                    _scale = null;
                }
            }

            /** The data property to bind to. */
            public function get property():String { return _property; }
            public function set property(name:String):void {
                if (name != _property) {
                    _property = name;
                    _stats = null;
                    _scale = null;
                }
            }

            /** The underlying scale created by this binding. */
            protected function get scale():Scale {
                if (!_data || !_group || !_property) {
                    throw new Error("Can't create scale with data to bind to.");
                }
                if (!_scale) {
                    _stats = _data.group(_group).stats(_property);
                    _scale = buildScale(_stats);
                }
                return _scale;
            }

            // --------------------------------------------------------------------

            /**
             * Creates a new ScaleBinding.
             */
            public function ScaleBinding()
            {
            }

            /**
             * Checks to see if the binding is current. If not, the internal stats
             * and scale for this binding will be cleared and lazily recomputed.
             * @return true if the binding was updated, false otherwise
             */
            public function updateBinding():Boolean
            {
                if (ignoreUpdates) return false;
                var stats:Stats = _data.group(_group).stats(_property);
                if (stats !== _stats) { // object identity test
                    _stats = null;
                    _scale = null;
                    return true;
                }
                return false;
            }

            /**
             * Internal listener for data events that clears the current scale
             * instance as needed.
             * @param evt a DataEvent
             */
            private function onDataEvent(evt:DataEvent):void
            {
                if (ignoreUpdates) return;
                if (evt.list.name == _group) {
                    if (evt.type == DataEvent.UPDATE) {
                        updateBinding();
                    } else {
                        _stats = null;
                        _scale = null;
                    }
                }
            }

            /** @inheritDoc */
            public override function clone() : Scale
            {
                return scale.clone();
            }

            /**
             * Returns the index of the input value in the ordinal array if the
             * scale is ordinal or categorical, otherwise returns -1.
             * @param value the value to lookup
             * @return the index of the input value. If the value is not contained
             *  in the ordinal array, this method returns -1.
             */
            public function index(value:Object):int
            {
                var s:OrdinalScale = scale as OrdinalScale;
                return (s ? s.index(value) : -1);
            }

            /** The number of distinct values in this scale, if ordinal. */
            public function get length():int
            {
                var s:OrdinalScale = scale as OrdinalScale;
                return (s ? s.length : -1);
            }

            /** @inheritDoc */
            public override function interpolate(value:Object) : Number
            {
                return scale.interpolate(value);
            }

            /** @inheritDoc */
            public override function lookup(f:Number) : Object
            {
                return scale.lookup(f);
            }

            /** @inheritDoc */
            public override function values(num:int=-1) : Array
            {
                return scale.values(num);
            }

            /** @inheritDoc */
            public override function label(value:Object) : String
            {
                return scale.label(value);
            }

            /** @private */
            protected function buildScale(stats:Stats):Scale
            {
                var type:String = _scaleType ? _scaleType : ScaleType.UNKNOWN;
                var vals:Array = _ordinals ? _ordinals : stats.distinctValues;
                var scale:Scale;

                switch (stats.dataType) {
                    case Stats.NUMBER:
                        switch (type) {
                            case ScaleType.LINEAR:
                            case ScaleType.UNKNOWN:
                                scale = new LinearScale(stats.minimum, stats.maximum, _base, _flush, _format);
                                break;
                            case ScaleType.ROOT:
                                var pow:Number = isNaN(_power) ? 2 : _power;
                                scale = new RootScale(stats.minimum, stats.maximum, _base, _flush, pow, _format);
                                break;
                            case ScaleType.LOG:
                                scale = new LogScale(stats.minimum, stats.maximum, _base, _flush, _format);
                                break;
                            case ScaleType.QUANTILE:
                                scale = new QuantileScale(_bins, stats.values, true, _format);
                                break;
                            default:
                                scale = new OrdinalScale(vals, _flush, false, _format);
                                break;
                        }
                        break;
                    case Stats.DATE:
                        switch (type) {
                            case ScaleType.UNKNOWN:
                            case ScaleType.LINEAR:
                            case ScaleType.TIME:
                                scale = new TimeScale(stats.minDate, stats.maxDate, _flush, _format);
                                break;
                            default:
                                scale = new OrdinalScale(vals, _flush, false, _format);
                                break;
                        }
                        break;
                    default:
                        scale = new OrdinalScale(vals, _flush, false, _format);
                        break;
                }

                if (_pmin) scale.min = _pmin;
                if (_pmax) scale.max = _pmax;
                if (_zeroBased) zeroAlignScale(scale);

                return scale;
            }

            private static function zeroAlignScale(scale:Scale):void
            {
                if (scale is QuantitativeScale) {
                    var qs:QuantitativeScale = QuantitativeScale(scale);
                    if (qs.scaleMin > 0) qs.dataMin = 0;
                    if (qs.scaleMax < 0) qs.dataMax = 0;
                }
            }

        } // end of class ScaleBinding
    }