/**
 * @class Ext.sparkline.Base
 *
 * The base class for ExtJS SparkLines. SparkLines are small, inline graphs used to visually
 * display small amounts of data. For large datasets, use the
 * {@link Ext.chart.AbstractChart chart package}.
 *
 * The SparkLine subclasses accept an {@link #values array of values}, and present the data
 * in different visualizations.
 *
 *     @example
 *     new Ext.Panel({
 *         height: 300,
 *         width: 600,
 *         frame: true,
 *         title: 'Test Sparklines',
 *         renderTo:document.body,
 *         bodyPadding: 10,
 *
 *         // Named listeners will resolve to methods in this Panel
 *         defaultListenerScope: true,
 *
 *         // Named references will be collected, and can be access from this Panel
 *         referenceHolder: true,
 *
 *         items: [{
 *             reference: 'values',
 *             xtype: 'textfield',
 *             fieldLabel: 'Values',
 *             validator: function(v) {
 *                 var result = [];
 *
 *                 v = v.replace(/\s/g, '');
 *                 v = v.replace(/,$/, '');
 *                 v = v.split(',');
 *                 for (var i = 0; i < v.length; i++) {
 *                     if (!Ext.isNumeric(v[i])) {
 *                         return 'Value must be a comma separated array of numbers';
 *                     }
 *                     result.push(parseInt(v[i], 10));
 *                 }
 *                 this.values = result;
 *                 return true;
 *             },
 *             listeners: {
 *                 change: 'onTypeChange',
 *                 buffer: 500,
 *                 afterrender: {
 *                     fn: 'afterTypeRender',
 *                     single: true
 *                 }
 *             }
 *         }, {
 *             reference: 'type',
 *             xtype: 'combobox',
 *             fieldLabel: 'Type',
 *             store: [
 *                 ['sparklineline',     'Line'],
 *                 ['sparklinebox',      'Box'],
 *                 ['sparklinebullet',   'Bullet'],
 *                 ['sparklinediscrete', 'Discrete'],
 *                 ['sparklinepie',      'Pie'],
 *                 ['sparklinetristate', 'TriState']
 *             ],
 *             value: 'sparklineline',
 *             listeners: {
 *                 change: 'onTypeChange',
 *                 buffer: 500
 *             }
 *         }],
 *
 *         // Start with a line plot. 
 *         afterTypeRender: function(typeField) {
 *             typeField.setValue('6,10,4,-3,7,2');
 *         },
 *
 *         onTypeChange: function() {
 *             var me = this,
 *                 refs = me.getReferences(),
 *                 config;
 *
 *             if (me.sparkLine) {
 *                 me.remove(me.sparkLine, true);
 *             }
 *             config = {
 *                 xtype: refs.type.getValue(),
 *                 values: refs.values.values,
 *                 height: 25,
 *                 width: 100                    
 *             };
 *            me.sparkLine = Ext.create(config);
 *             me.add(me.sparkLine);
 *
 *             // Put under fields
 *             me.sparkLine.el.dom.style.marginLeft = refs.type.labelEl.getWidth() + 'px';
 *         }
 *     });
 *
 */
Ext.define('Ext.sparkline.Base', {
    extend: 'Ext.Gadget',
    xtype: 'sparkline',
    requires: [
        'Ext.XTemplate',
        'Ext.sparkline.CanvasCanvas',
        'Ext.sparkline.VmlCanvas',
        'Ext.util.Color'
    ],
 
    cachedConfig: {
        /**
         * @cfg {String} lineColor
         * The hex value for line colors in graphs which
         * display lines ({@link Ext.sparkline.Box Box},
         * {@link Ext.sparkline.Discrete Discrete} and {@link Ext.sparkline.Line Line}).
         */
        lineColor: '#157fcc',
 
        defaultPixelsPerValue: 3,
 
        tagValuesAttribute: 'values',
 
        enableTagOptions: false,
        
        enableHighlight: true,
        
        /**
         * @cfg {String} [highlightColor=null]
         * The hex value for the highlight color to use when mouseing over a graph segment.
         */
        highlightColor: null,
        
        /**
         * @cfg {Number} [highlightLighten]
         * How much to lighten the highlight color by when mouseing over a graph segment.
         */
        highlightLighten: 0.1,
        
        /**
         * @cfg {Boolean} [tooltipSkipNull=true]
         * Null values will not have a tooltip displayed.
         */
        tooltipSkipNull: true,
        
        /**
         * @cfg {String} [tooltipPrefix]
         * A string to prepend to each field displayed in a tooltip.
         */
        tooltipPrefix: '',
        
        /**
         * @cfg {String} [tooltipSuffix]
         * A string to append to each field displayed in a tooltip.
         */
        tooltipSuffix: '',
        
        /**
         * @cfg {Boolean} [disableTooltips=false]
         * Set to `true` to disable mouseover tooltips.
         */
        disableTooltips: false,
        
        disableInteraction: false,
        
        /**
         * @cfg {String/Ext.XTemplate} [tipTpl]
         * An XTemplate used to display the value or values in a tooltip when hovering
         * over a Sparkline.
         *
         * The implemented subclases all define their own `tipTpl`, but it can be overridden.
         */
        tipTpl: null
    },
 
    config: {
        /**
         * @cfg {Number[]} values An array of numbers which define the chart.
         */
        values: null
    },
 
    baseCls: Ext.baseCSSPrefix + 'sparkline',
 
    element: {
        tag: 'canvas',
        reference: 'element',
        style: {
            display: 'inline-block',
            verticalAlign: 'top'
        },
        listeners: {
            mouseenter: 'onMouseEnter',
            mouseleave: 'onMouseLeave',
            mousemove: 'onMouseMove'
        },
        // Create canvas zero sized so that it does not affect the containing element's
        // initial layout
        // https://sencha.jira.com/browse/EXTJSIV-10145
        width: 0,
        height: 0
    },
    
    defaultBindProperty: 'values',
 
    // When any config is changed, the canvas needs to be redrawn.
    // This is done at the next animation frame when this queue is traversed.
    redrawQueue: {},
 
    inheritableStatics: {
        /**
         * @private
         * @static
         * @inheritable
         */
        onClassCreated: function(cls) {
            var configUpdater = cls.prototype.updateConfigChange,
                proto = cls.prototype,
                configs = cls.getConfigurator().configs,
                config,
                updaterName;
 
            // Set up an applier for all local configs which kicks off a request to redraw
            // on the next animation frame
            for (config in configs) {
                // tipTpl not included in this scheme
                if (config !== 'tipTpl') {
                    updaterName = Ext.Config.get(config).names.update;
 
                    if (proto[updaterName]) {
                        proto[updaterName] =
                            Ext.Function.createSequence(proto[updaterName], configUpdater);
                    }
                    else {
                        proto[updaterName] = configUpdater;
                    }
                }
            }
        }
    },
 
    constructor: function(config) {
        var me = this,
            ns = Ext.sparkline;
 
        // The canvas sets our element config
        me.canvas = Ext.supports.Canvas ? new ns.CanvasCanvas(me) : new ns.VmlCanvas(me);
 
        me.callParent([config]);
    },
 
    // determine if all values of an array match a value
    // returns true if the array is empty
    all: function(val, arr, ignoreNull) {
        var i;
 
        for (= arr.length; i--;) {
            if (ignoreNull && arr[i] === null) {
                continue;
            }
 
            if (arr[i] !== val) {
                return false;
            }
        }
 
        return true;
    },
 
    // generic config value updater.
    // Redraws the graph, unless we are configured to buffer redraws
    // in wehich case it adds this to the queue to do a redraw on the next animation frame.
    updateConfigChange: function(newValue) {
        var me = this;
 
        // If we are buffering until animation frame, or we have not been sized, then
        // queue a redraw flush.
        if (me.bufferRedraw || !me.height || !me.width) {
            me.redrawQueue[me.getId()] = me;
 
            // Ensure that there is a single timer to handle all queued redraws.
            if (!me.redrawTimer) {
                Ext.sparkline.Base.prototype.redrawTimer =
                        Ext.raf(me.processRedrawQueue);
            }
        }
        else {
            me.redraw();
        }
 
        return newValue;
    },
 
    // Appliers convert an incoming config value.
    // Ensure the tipTpl is an XTemplate
    applyTipTpl: function(tipTpl) {
        if (tipTpl && !tipTpl.isTemplate) {
            tipTpl = new Ext.XTemplate(tipTpl);
        }
 
        return tipTpl;
    },
 
    normalizeValue: function(val) {
        var nf;
 
        switch (val) {
            case 'undefined':
                val = undefined;
                break;
            case 'null':
                val = null;
                break;
            case 'true':
                val = true;
                break;
            case 'false':
                val = false;
                break;
 
            default:
                nf = parseFloat(val);
 
                if (val == nf) { // eslint-disable-line eqeqeq
                    val = nf;
                }
        }
 
        return val;
    },
 
    normalizeValues: function(vals) {
        var i,
            result = [];
 
        for (= vals.length; i--;) {
            result[i] = this.normalizeValue(vals[i]);
        }
 
        return result;
    },
 
    updateWidth: function(width, oldWidth) {
        var me = this,
            dom = me.element.dom,
            measurer = me.measurer;
 
        me.callParent([width, oldWidth]);
        me.canvas.setWidth(width);
        me.width = width;
 
        if (me.height == null && measurer) {
            me.setHeight(parseInt(measurer.getCachedStyle(dom.parentNode, 'line-height'), 10));
        }
    },
 
    updateHeight: function(height, oldHeight) {
        var me = this;
 
        me.callParent([height, oldHeight]);
        me.canvas.setHeight(height);
        me.height = height;
    },
 
    setValues: function(values) {
        var me = this,
            oldValues = me.getValues();
 
        // the values config is expected to be an array
        values = values == null ? [] : Ext.Array.from(values);
        me.values = values;
        me.callParent([values]);
 
        // If it's physically the same Array object, we need to invoke the updater
        // because though the array is the same, it may have been mutated,
        // and the config system setter will reject the change and not invoke the updater.
        // This is how the Stock Ticker example works. It shuffles values down
        // a static array.
        if (values === oldValues) {
            me.updateValues([values, oldValues]);
        }
    },
 
    redraw: function() {
        var me = this;
 
        if (!me.destroyed) {
            me.canvas.onOwnerUpdate();
            me.canvas.reset();
 
            if (me.getValues()) {
                me.onUpdate();
                me.renderGraph();
            }
        }
    },
 
    onUpdate: Ext.emptyFn,
 
    /*
     * Render the chart to the canvas
     */
    renderGraph: function() {
        var ret = true;
 
        if (this.disabled) {
            this.canvas.reset();
            ret = false;
        }
 
        return ret;
    },
 
    onMouseEnter: function(e) {
        this.onMouseMove(e);
    },
 
    onMouseMove: function(e) {
        var me = this;
 
        // In IE/Edge, the mousemove event fires before mouseenter
        // This is correct according to the spec
        // https://www.w3.org/TR/uievents/#events-mouseevent-event-order
        me.canvasRegion = me.canvasRegion || me.canvas.el.getRegion();
        me.currentPageXY = e.getPoint();
        me.redraw();
    },
 
    onMouseLeave: function() {
        var me = this;
 
        // mouseleave is guaranteed to fire last, so clear region here
        me.canvasRegion = me.currentPageXY = me.targetX = me.targetY = null;
        me.redraw();
        me.hideTip();
    },
 
    updateDisplay: function() {
        var me = this,
            values = me.getValues(),
            tipHtml, region;
 
        // To work out the position of currentPageXY within the canvas, we must account
        // for the fact that while document Y values as represented in the currentPageXY
        // are based from the top of the document, canvas Y values begin from the bottom
        // of the canvas element.
        if (values && values.length && me.currentPageXY &&
            me.canvasRegion.contains(me.currentPageXY)) {
            region = me.getRegion(me.currentPageXY[0] - me.canvasRegion.left,
                                  (me.canvasRegion.bottom - 1) - me.currentPageXY[1]);
 
            if (region != null && me.isValidRegion(region, values)) {
                if (!me.disableHighlight) {
                    me.renderHighlight(region);
                }
 
                if (!me.getDisableTooltips()) {
                    tipHtml = me.getRegionTooltip(region);
                }
            }
 
            if (me.hasListeners.sparklineregionchange) {
                me.fireEvent('sparklineregionchange', me);
            }
 
            if (tipHtml) {
                me.getSharedTooltip().setHtml(tipHtml);
                me.showTip();
            }
        }
 
        // No tip content; ensure it's hidden
        if (!tipHtml) {
            me.hideTip();
        }
    },
 
    /**
     * @method
     * Return a region id for a given x/y co-ordinate
     */
    getRegion: Ext.emptyFn,
 
    /**
     * Fetch the HTML to display as a tooltip
     */
    getRegionTooltip: function(region) {
        var me = this,
            entries = [],
            tipTpl = me.getTipTpl(),
            fields, showFields, showFieldsKey, newFields, fv,
            formatter, fieldlen, i, j;
 
        fields = me.getRegionFields(region);
        formatter = me.tooltipFormatter;
 
        if (formatter) {
            return formatter(me, me, fields);
        }
 
        if (!tipTpl) {
            return '';
        }
 
        if (!Ext.isArray(fields)) {
            fields = [fields];
        }
 
        showFields = me.tooltipFormatFieldlist;
        showFieldsKey = me.tooltipFormatFieldlistKey;
 
        if (showFields && showFieldsKey) {
            // user-selected ordering of fields
            newFields = [];
 
            for (= fields.length; i--;) {
                fv = fields[i][showFieldsKey];
 
                if ((= Ext.Array.indexOf(fv, showFields)) !== -1) {
                    newFields[j] = fields[i];
                }
            }
 
            fields = newFields;
        }
 
        fieldlen = fields.length;
 
        for (= 0; j < fieldlen; j++) {
            if (!fields[j].isNull || !me.getTooltipSkipNull()) {
                Ext.apply(fields[j], {
                    prefix: me.getTooltipPrefix(),
                    suffix: me.getTooltipSuffix()
                });
                entries.push(tipTpl.apply(fields[j]));
            }
        }
 
        if (entries.length) {
            return entries.join('<br>');
        }
 
        return '';
    },
 
    getRegionFields: Ext.emptyFn,
 
    calcHighlightColor: function(color) {
        var me = this,
            highlightColor = me.getHighlightColor(),
            lighten = me.getHighlightLighten(),
            o;
 
        if (highlightColor) {
            return highlightColor;
        }
 
        if (lighten) {
            o = Ext.util.Color.fromString(color);
 
            if (o) {
                o.lighten(lighten);
                color = o.toHex();
            }
        }
 
        return color;
    },
 
    destroy: function() {
        delete this.redrawQueue[this.getId()];
        this.callParent();
    },
 
    privates: {
        hideTip: Ext.privateFn,
 
        isValidRegion: function(region, values) {
            return region < values.length;
        },
 
        showTip: Ext.privateFn
    }
}, function(SparklineBase) {
    var proto = SparklineBase.prototype;
 
    proto.getSharedTooltip = function() {
        var me = this,
            tooltip = me.tooltip;
 
        if (!tooltip) {
            proto.tooltip = tooltip = SparklineBase.constructTip();
        }
 
        return tooltip;
    };
 
    SparklineBase.onClassCreated(SparklineBase);
    
    proto.processRedrawQueue = function() {
        var redrawQueue = proto.redrawQueue,
            id;
 
        for (id in redrawQueue) {
            redrawQueue[id].redraw();
        }
 
        proto.redrawQueue = {};
        proto.redrawTimer = 0;
    };
});