/**
 * A class that maps data values to colors.
 */
Ext.define('Ext.d3.axis.Color', {

    requires: [
        'Ext.d3.Helpers'
    ],

    mixins: {
        observable: 'Ext.mixin.Observable'
    },

    isColorAxis: true,

    config: {
        /**
         * @cfg {Function} scale
         * A D3 [scale](https://github.com/d3/d3/wiki/Scales) with a color range.
         * This config is configured similarly to the {@link Ext.d3.axis.Axis#scale}
         * config.
         * @cfg {Array} scale.domain The `domain` to use. If not set (default),
         * the domain will be automatically calculated based on data.
         */
        scale: {
            // Notes about config merging effects for scales.
            // For example, if default `scale` config for this class is:
            //
            //     scale: {
            //         type: 'linear',
            //         range: ['white', 'maroon']
            //     }
            //
            // and component's `colorAxis` config is
            //
            //    colorAxis: {
            //        scale: {
            //            type: 'ordinal',
            //            range: 'd3.schemeCategory20c'
            //        }
            //    }
            //
            // the `category20` scale will be created, defined by D3 as:
            //
            //     d3.scale.ordinal().range(d3_category20)
            //
            // but because the configs are merged, the ['white', 'maroon'] range will also apply
            // to the new `category20` scale, which defeats the purpose of using this scale.
            // So we only allow config merging for scales of the same type.
            merge: function(value, baseValue) {
                if (value && value.type && baseValue && baseValue.type === value.type) {
                    value = Ext.Object.merge(baseValue, value);
                }

                return value;
            },
            $value: {
                type: 'linear',
                range: ['white', 'maroon']
            }
        },

        /**
         * @cfg {String} field
         * The field that will be used to fetch the value,
         * when a {@link Ext.data.Model} instance is passed to the {@link #getColor} method.
         */
        field: null,

        /**
         * @cfg {Function} processor
         * Custom value processor.
         * @param {Ext.d3.axis.Color} axis
         * @param {Function} scale
         * @param {*} value The type will depend on component used.
         * @param {String} field
         * @return {String} color
         */
        processor: null,

        /**
         * @cfg {Number} minimum The minimum data value.
         * The data domain is calculated automatically, setting this config to a number
         * will override the calculated minimum value.
         */
        minimum: null,

        /**
         * @cfg {Number} maximum The maximum data value.
         * The data domain is calculated automatically, setting this config to a number
         * will override the calculated maximum value.
         */
        maximum: null
    },

    constructor: function(config) {
        this.mixins.observable.constructor.call(this, config);
    },

    applyScale: function(scale, oldScale) {
        if (scale) {
            if (!Ext.isFunction(scale)) {
                this.isUserSetDomain = !!scale.domain;
                scale = Ext.d3.Helpers.makeScale(scale);
            }
        }

        return scale || oldScale;
    },

    updateScale: function(scale) {
        this.scale = scale;
    },

    updateField: function(field) {
        this.field = field;
    },

    updateProcessor: function(processor) {
        this.processor = processor;
    },

    /**
     * Maps the given `value` to a color.
     * In case the `value` is a {@link Ext.data.Model} instance, the {@link #field}
     * config will be used to fetch the actual value.
     * @param {*} value
     * @return {String}
     */
    getColor: function(value) {
        var scale = this.scale,
            field = this.field,
            processor = this.processor,
            color;

        if (processor) {
            color = processor(this, scale, value, field);
        }
        else if (value && value.data && value.data.isModel && field) {
            color = scale(value.data.data[field]);
        }
        else {
            color = scale(value);
        }

        return color;
    },

    updateMinimum: function() {
        this.setDomain();
    },

    updateMaximum: function() {
        this.setDomain();
    },

    /**
     * @private
     * For quantitative scales only!
     */
    findDataDomain: function(records) {
        var field = this.field,
            min = d3.min(records, function(record) {
                return record.data[field];
            }),
            max = d3.max(records, function(record) {
                return record.data[field];
            }),
            domain;

        if (field) {
            if (Ext.isNumber(min) && Ext.isNumber(max)) {
                domain = [min, max];
            }
            else {
                // ordinal data
                domain = records.map(function(record) {
                    return record.data[field];
                }).sort().filter(function(item, index, array) {
                    // Remove duplicates by sorting and removing
                    // each element equal to the preceding one.
                    return !index || item !== array[index - 1];
                });
            }
        }

        return domain;
    },

    /**
     * @private
     * For quantitative scales only!
     */
    setDomainFromData: function(models) {
        if (!this.isUserSetDomain) {
            this.setDomain(this.findDataDomain(models));
        }
    },

    /**
     * @private
     * For quantitative scales only!
     * Sets the domain of the {@link #scale} taking into account the
     * {@link #minimum} and {@link #maximum}. If no `domain` is given,
     * updates the current domain.
     * @param {Number[]} [domain]
     */
    setDomain: function(domain) {
        var me = this,
            scale = me.getScale(),
            range = scale.range(),
            rangeLength = range.length,
            minimum = me.getMinimum(),
            maximum = me.getMaximum(),
            step, start, end, i;

        if (scale && !me.isUserSetDomain) {
            if (!domain) {
                domain = scale.domain();
            }

            domain = domain.slice();

            // Domain is an array of two or more numbers.
            if (Ext.isNumber(minimum)) {
                domain[0] = minimum;
            }

            if (Ext.isNumber(maximum)) {
                domain[domain.length - 1] = maximum;
            }

            if (scale.ticks && domain.length !== rangeLength) {
                // make polylinear color scale
                start = domain[0];
                end = domain[domain.length - 1];
                step = (end - start) / (rangeLength - 1);
                domain = [];

                for (i = 0; i < rangeLength - 1; i++) {
                    domain.push(start + i * step);
                }

                domain.push(end);
            }

            scale.domain(domain);

            me.fireEvent('scalechange', me, scale);
        }
    }

});
