/**
 * The Ext.chart package provides the capability to visualize data.
 * Each chart binds directly to an {@link Ext.data.Store} enabling automatic updates of the chart.
 * A chart configuration object has some overall styling options as well as an array of axes
 * and series. A chart instance example could look like:
 *
 *     new Ext.chart.CartesianChart({
 *         width: 800,
 *         height: 600,
 *         animate: true,
 *         store: store1,
 *         legend: {
 *             position: 'right'
 *         },
 *         axes: [
 *             // ...some axes options...
 *         ],
 *         series: [
 *             // ...some series options...
 *         ]
 *     });
 *
 * In this example we set the `width` and `height` of a cartesian chart; We decide whether our series are
 * animated or not and we select a store to be bound to the chart; We also set the legend to the right part of the
 * chart.
 *
 * You can register certain interactions such as {@link Ext.chart.interactions.PanZoom} on the chart by specify an
 * array of names or more specific config objects. All the events will be wired automatically.
 *
 * You can also listen to `itemXXX` events directly on charts. That case all the contained series will relay this event to the
 * chart.
 *
 * For more information about the axes and series configurations please check the documentation of
 * each series (Line, Bar, Pie, etc).
 *
 */

Ext.define('Ext.chart.AbstractChart', {

    extend: 'Ext.draw.Component',

    requires: [
        'Ext.chart.series.Series',
        'Ext.chart.interactions.Abstract',
        'Ext.chart.axis.Axis',
        'Ext.data.StoreManager',
        'Ext.chart.Legend',
        'Ext.data.Store',
        'Ext.draw.engine.SvgExporter'
    ],
    /**
     * @event beforerefresh
     * Fires before a refresh to the chart data is called.  If the `beforerefresh` handler returns
     * `false` the {@link #refresh} action will be canceled.
     * @param {Ext.chart.AbstractChart} this
     */

    /**
     * @event refresh
     * Fires after the chart data has been refreshed.
     * @param {Ext.chart.AbstractChart} this
     */

    /**
     * @event redraw
     * Fires after the chart is redrawn.
     * @param {Ext.chart.AbstractChart} this
     */

    /**
     * @event itemmousemove
     * Fires when the mouse is moved on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemmouseup
     * Fires when a mouseup event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemmousedown
     * Fires when a mousedown event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemmouseover
     * Fires when the mouse enters a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemmouseout
     * Fires when the mouse exits a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemclick
     * Fires when a click event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemdoubleclick
     * Fires when a doubleclick event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemtap
     * Fires when a tap event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemtapstart
     * Fires when a tapstart event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemtapend
     * Fires when a tapend event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemtapcancel
     * Fires when a tapcancel event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemtaphold
     * Fires when a taphold event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemdoubletap
     * Fires when a doubletap event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemsingletap
     * Fires when a singletap event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemtouchstart
     * Fires when a touchstart event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemtouchmove
     * Fires when a touchmove event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemtouchend
     * Fires when a touchend event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemdragstart
     * Fires when a dragstart event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemdrag
     * Fires when a drag event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemdragend
     * Fires when a dragend event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itempinchstart
     * Fires when a pinchstart event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itempinch
     * Fires when a pinch event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itempinchend
     * Fires when a pinchend event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */
    /**
     * @event itemswipe
     * Fires when a swipe event occurs on a series item.
     * @param {Ext.chart.series.Series} series
     * @param {Object} item
     * @param {Event} event
     */

    /**
     * @property version Current Version of Touch Charts
     * @type {String}
     */
    version: '2.0.0',

    // @ignore
    viewBox: false,

    delegationRegex: /^item([a-z]+)$/i,

    domEvents: /click|focus|blur|paste|input|mousemove|mousedown|mouseup|mouseover|mouseout|keyup|keydown|keypress|submit|pinch|pinchstart|pinchend|touchstart|touchend|rotate|rotatestart|rotateend|drag|dragstart|dragend|tap|doubletap|singletap/,

    config: {

        /**
         * @cfg {Ext.data.Store} store
         * The store that supplies data to this chart.
         */
        store: null,

        /**
         * @cfg {Boolean/Object} shadow (optional) `true` for the default shadow configuration `{shadowOffsetX: 2, shadowOffsetY: 2, shadowBlur: 3, shadowColor: '#444'}`
         * or a standard shadow config object to be used for default chart shadows.
         */
        shadow: false,

        /**
         * @cfg {Boolean/Object} animate (optional) `true` for the default animation (easing: 'ease' and duration: 500)
         * or a standard animation config object to be used for default chart animations.
         */
        animate: true,

        /**
         * @cfg {Ext.chart.series.Series/Array} series
         * Array of {@link Ext.chart.series.Series Series} instances or config objects. For example:
         *
         *     series: [{
         *         type: 'column',
         *         axis: 'left',
         *         listeners: {
         *             'afterrender': function() {
         *                 console.log('afterrender');
         *             }
         *         },
         *         xField: 'category',
         *         yField: 'data1'
         *     }]
         */
        series: [],

        /**
         * @cfg {Ext.chart.axis.Axis/Array/Object} axes
         * Array of {@link Ext.chart.axis.Axis Axis} instances or config objects. For example:
         *
         *     axes: [{
         *         type: 'numeric',
         *         position: 'left',
         *         title: 'Number of Hits',
         *         minimum: 0
         *     }, {
         *         type: 'category',
         *         position: 'bottom',
         *         title: 'Month of the Year'
         *     }]
         */
        axes: [],

        /**
         * @cfg {Ext.chart.Legend/Object} legend
         */
        legend: null,

        /**
         * @cfg {Boolean/Array} colors Array of colors/gradients to override the color of items and legends.
         */
        colors: null,

        /**
         * @cfg {Object|Number} insetPadding The amount of inset padding in pixels for the chart. Inset padding is
         * the padding from the boundary of the chart to any of its contents.
         * @cfg {Number} insetPadding.top
         */
        insetPadding: {
            top: 10,
            left: 10,
            right: 10,
            bottom: 10
        },

        /**
         * @cfg {Object} innerPadding The amount of inner padding in pixel. Inner padding is the padding from
         * axis to the series.
         */
        innerPadding: {
            top: 0,
            left: 0,
            right: 0,
            bottom: 0
        },

        /**
         * @cfg {Object} background Set the chart background. This can be a gradient object, image, or color.
         *
         * For example, if `background` were to be a color we could set the object as
         *
         *     background: {
         *         //color string
         *         fill: '#ccc'
         *     }
         *
         * You can specify an image by using:
         *
         *     background: {
         *         image: 'http://path.to.image/'
         *     }
         *
         * Also you can specify a gradient by using the gradient object syntax:
         *
         *     background: {
         *         gradient: {
         *             type: 'linear',
         *             angle: 45,
         *             stops: {
         *                 0: {
         *                     color: '#555'
         *                 },
         *                 100: {
         *                     color: '#ddd'
         *                 }
         *             }
         *         }
         *     }
         */
        background: null,

        /**
         * @cfg {Array} interactions
         * Interactions are optional modules that can be plugged in to a chart to allow the user to interact
         * with the chart and its data in special ways. The `interactions` config takes an Array of Object
         * configurations, each one corresponding to a particular interaction class identified by a `type` property:
         *
         *     new Ext.chart.AbstractChart({
         *         renderTo: Ext.getBody(),
         *         width: 800,
         *         height: 600,
         *         store: store1,
         *         axes: [
         *             // ...some axes options...
         *         ],
         *         series: [
         *             // ...some series options...
         *         ],
         *         interactions: [{
         *             type: 'interactiontype'
         *             // ...additional configs for the interaction...
         *         }]
         *     });
         *
         * When adding an interaction which uses only its default configuration (no extra properties other than `type`),
         * you can alternately specify only the type as a String rather than the full Object:
         *
         *     interactions: ['reset', 'rotate']
         *
         * The current supported interaction types include:
         *
         * - {@link Ext.chart.interactions.PanZoom panzoom} - allows pan and zoom of axes
         * - {@link Ext.chart.interactions.ItemHighlight itemhighlight} - allows highlighting of series data points
         * - {@link Ext.chart.interactions.ItemInfo iteminfo} - allows displaying details of a data point in a popup panel
         * - {@link Ext.chart.interactions.Rotate rotate} - allows rotation of pie and radar series
         *
         * See the documentation for each of those interaction classes to see how they can be configured.
         *
         * Additional custom interactions can be registered using `'interactions.'` alias prefix.
         */
        interactions: [],

        /**
         * @private
         * The main region of the chart.
         */
        mainRegion: null,

        /**
         * @private
         * Override value
         */
        autoSize: false,

        /**
         * @private
         * Override value
         */
        viewBox: false,

        /**
         * @private
         * Override value
         */
        fitSurface: false,

        /**
         * @private
         * Override value
         */
        resizeHandler: null,

        /**
         * @readonly
         * @cfg {Object} highlightItem
         * The current highlight item in the chart.
         * The object must be the one that you get from item events.
         *
         * Note that series can also own highlight items.
         * This notion is separate from this one and should not be used at the same time.
         */
        highlightItem: null
    },

    /**
     * @private
     */
    resizing: 0,

    /**
     * Toggle for chart interactions that require animation to be suspended.
     * @private
     */
    animationSuspended: 0,

    /**
     * @private The z-indexes to use for the various surfaces
     */
    surfaceZIndexes: {
        main: 0,
        grid: 1,
        series: 2,
        axis: 3,
        overlay: 4,
        events: 5
    },

    animating: 0,

    layoutSuspended: 0,

    applyAnimate: function (newAnimate, oldAnimate) {
        if (!newAnimate) {
            newAnimate = {
                duration: 0
            };
        } else if (newAnimate === true) {
            newAnimate = {
                easing: 'easeInOut',
                duration: 500
            };
        }
        if (!oldAnimate) {
            return newAnimate;
        } else {
            oldAnimate = Ext.apply({}, newAnimate, oldAnimate);
        }
        return oldAnimate;
    },

    applyInsetPadding: function (padding, oldPadding) {
        if (Ext.isNumber(padding)) {
            return {
                top: padding,
                left: padding,
                right: padding,
                bottom: padding
            };
        } else if (!oldPadding) {
            return padding;
        } else {
            return Ext.apply(oldPadding, padding);
        }
    },

    applyInnerPadding: function (padding, oldPadding) {
        if (Ext.isNumber(padding)) {
            return {
                top: padding,
                left: padding,
                right: padding,
                bottom: padding
            };
        } else if (!oldPadding) {
            return padding;
        } else {
            return Ext.apply(oldPadding, padding);
        }
    },

    suspendAnimation: function () {
        this.animationSuspended++;
        if (this.animationSuspended === 1) {
            var series = this.getSeries(), i = -1, n = series.length;
            while (++i < n) {
                //update animation config to not animate
                series[i].setAnimate(this.getAnimate());
            }
        }
    },

    resumeAnimation: function () {
        this.animationSuspended--;
        if (this.animationSuspended === 0) {
            var series = this.getSeries(), i = -1, n = series.length;
            while (++i < n) {
                //update animation config to animate
                series[i].setAnimate(this.getAnimate());
            }
        }
    },

    suspendLayout: function () {
        this.layoutSuspended++;
        if (this.layoutSuspended === 1) {
            if (this.scheduledLayoutId) {
                this.layoutInSuspension = true;
                this.cancelLayout();
            } else {
                this.layoutInSuspension = false;
            }
        }
    },

    resumeLayout: function () {
        this.layoutSuspended--;
        if (this.layoutSuspended === 0) {
            if (this.layoutInSuspension) {
                this.scheduleLayout();
            }
        }
    },

    /**
     * Cancel a scheduled layout.
     */
    cancelLayout: function () {
        if (this.scheduledLayoutId) {
            Ext.draw.Animator.cancel(this.scheduledLayoutId);
            this.scheduledLayoutId = null;
        }
    },

    /**
     * Schedule a layout at next frame.
     */
    scheduleLayout: function () {
        if (!this.scheduledLayoutId) {
            this.scheduledLayoutId = Ext.draw.Animator.schedule('doScheduleLayout', this);
        }
    },

    doScheduleLayout: function () {
        if (this.layoutSuspended) {
            this.layoutInSuspension = true;
        } else {
            this.performLayout();
        }
    },

    getAnimate: function () {
        if (this.resizing || this.animationSuspended) {
            return {
                duration: 0
            };
        } else {
            return this._animate;
        }
    },

    constructor: function () {
        var me = this;
        me.itemListeners = {};
        me.surfaceMap = {};
        me.legendStore = new Ext.data.Store({
            storeId: this.getId() + '-legendStore',
            autoDestroy: true,
            fields: [
                'id', 'name', 'mark', 'disabled', 'series', 'index'
            ]
        });
        me.suspendLayout();
        me.callSuper(arguments);
        me.refreshLegendStore();
        me.getLegendStore().on('updaterecord', 'onUpdateLegendStore', me);
        me.resumeLayout();
    },

    /**
     * Return the legend store that contains all the legend information. These
     * information are collected from all the series.
     * @return {Ext.data.Store}
     */
    getLegendStore: function () {
        return this.legendStore;
    },

    refreshLegendStore: function () {
        if (this.getLegendStore()) {
            var i, ln,
                series = this.getSeries(), seriesItem,
                legendData = [];
            if (series) {
                for (i = 0, ln = series.length; i < ln; i++) {
                    seriesItem = series[i];
                    if (seriesItem.getShowInLegend()) {
                        seriesItem.provideLegendInfo(legendData);
                    }
                }
            }
            this.getLegendStore().setData(legendData);
        }
    },

    resetLegendStore: function () {
        if (this.getLegendStore()) {
            var data = this.getLegendStore().getData().items,
                i, ln = data.length,
                record;
            for (i = 0; i < ln; i++) {
                record = data[i];
                record.beginEdit();
                record.set('disabled', false);
                record.commit();
            }
        }
    },

    onUpdateLegendStore: function (store, record) {
        var series = this.getSeries(), seriesItem;
        if (record && series) {
            seriesItem = series.map[record.get('series')];
            if (seriesItem) {
                seriesItem.setHiddenByIndex(record.get('index'), record.get('disabled'));
                this.redraw();
            }
        }
    },

    initialize: function () {
        var me = this;
        me.callSuper();
        me.getSurface('main');
        me.getSurface('overlay-surface', 'overlay').waitFor(me.getSurface('series-surface', 'series'));
    },

    resizeHandler: function (size) {
        var me = this;
        me.scheduleLayout();
        return false;
    },

    applyMainRegion: function (newRegion, region) {
        if (!region) {
            return newRegion;
        }
        this.getSeries();
        this.getAxes();
        if (newRegion[0] === region[0] &&
            newRegion[1] === region[1] &&
            newRegion[2] === region[2] &&
            newRegion[3] === region[3]) {
            return region;
        } else {
            return newRegion;
        }
    },

    getSurface: function (name, type) {
        name = name || 'main';
        type = type || name;
        var me = this,
            surface = this.callSuper([name]),
            zIndexes = me.surfaceZIndexes;
        if (type in zIndexes) {
            surface.element.setStyle('zIndex', zIndexes[type]);
        }
        if (!me.surfaceMap[type]) {
            me.surfaceMap[type] = [];
        }
        surface.type = type;
        me.surfaceMap[type].push(surface);
        return surface;
    },

    updateColors: function (colors) {
        var series = this.getSeries(),
            seriesCount = series && series.length,
            seriesItem;
        for (var i = 0; i < seriesCount; i++) {
            seriesItem = series[i];
            if (!seriesItem.getColors()) {
                seriesItem.updateColors(colors);
            }
        }
    },

    applyAxes: function (newAxes, oldAxes) {
        this.resizing++;
        try {
            if (!oldAxes) {
                oldAxes = [];
                oldAxes.map = {};
            }
            var result = [], i, ln, axis, oldAxis, oldMap = oldAxes.map;
            result.map = {};
            newAxes = Ext.Array.from(newAxes, true);
            for (i = 0, ln = newAxes.length; i < ln; i++) {
                axis = newAxes[i];
                if (!axis) {
                    continue;
                }
                axis = Ext.factory(axis, null, oldAxis = oldMap[axis.getId && axis.getId() || axis.id], 'axis');
                if (axis) {
                    axis.setChart(this);
                    result.push(axis);
                    result.map[axis.getId()] = axis;
                    if (!oldAxis) {
                        axis.on('animationstart', 'onAnimationStart', this);
                        axis.on('animationend', 'onAnimationEnd', this);
                    }
                }
            }

            for (i in oldMap) {
                if (!result.map[i]) {
                    oldMap[i].destroy();
                }
            }
            return result;
        } finally {
            this.resizing--;
        }
    },

    updateAxes: function (newAxes) {
        this.scheduleLayout();
    },

    applySeries: function (newSeries, oldSeries) {
        this.resizing++;
        try {
            this.getAxes();
            if (!oldSeries) {
                oldSeries = [];
                oldSeries.map = {};
            }
            var me = this,
                result = [],
                i, ln, series, oldMap = oldSeries.map, oldSeriesItem;
            result.map = {};
            newSeries = Ext.Array.from(newSeries, true);
            for (i = 0, ln = newSeries.length; i < ln; i++) {
                series = newSeries[i];
                if (!series) {
                    continue;
                }
                oldSeriesItem = oldSeries.map[series.getId && series.getId() || series.id];
                if (series instanceof Ext.chart.series.Series) {
                    if (oldSeriesItem !== series) {
                        // Replacing
                        if (oldSeriesItem) {
                            oldSeriesItem.destroy();
                        }
                        me.addItemListenersToSeries(series);
                    }
                    series.setChart(this);
                } else if (Ext.isObject(series)) {
                    if (oldSeriesItem) {
                        // Update
                        oldSeriesItem.setConfig(series);
                        series = oldSeriesItem;
                    } else {
                        // Create a series.
                        if (Ext.isString(series)) {
                            series = Ext.create(series.xclass || ("series." + series), {chart: this});
                        } else {
                            series.chart = this;
                            series = Ext.create(series.xclass || ("series." + series.type), series);
                        }
                        series.on('animationstart', 'onAnimationStart', this);
                        series.on('animationend', 'onAnimationEnd', this);
                        me.addItemListenersToSeries(series);
                    }
                }

                result.push(series);
                result.map[series.getId()] = series;
            }

            for (i in oldMap) {
                if (!result.map[oldMap[i].getId()]) {
                    oldMap[i].destroy();
                }
            }
            return result;
        } finally {
            this.resizing--;
        }
    },

    applyLegend: function (newLegend, oldLegend) {
        return Ext.factory(newLegend, Ext.chart.Legend, oldLegend);
    },

    updateLegend: function (legend) {
        if (legend) {
            // On create
            legend.setStore(this.getLegendStore());
            if (!legend.getDocked()) {
                legend.setDocked('bottom');
            }
            if (this.getParent()) {
                this.getParent().add(legend);
            }
        }
    },

    setParent: function (parent) {
        this.callSuper(arguments);
        if (parent && this.getLegend()) {
            parent.add(this.getLegend());
        }
    },

    updateSeries: function (newSeries, oldSeries) {
        this.resizing++;
        try {
            this.fireEvent('serieschanged', this, newSeries, oldSeries);
            this.refreshLegendStore();
            this.scheduleLayout();
        } finally {
            this.resizing--;
        }
    },

    applyInteractions: function (interactions, oldInteractions) {
        if (!oldInteractions) {
            oldInteractions = [];
            oldInteractions.map = {};
        }
        var me = this,
            result = [], oldMap = oldInteractions.map,
            i, ln, interaction;
        result.map = {};
        interactions = Ext.Array.from(interactions, true);
        for (i = 0, ln = interactions.length; i < ln; i++) {
            interaction = interactions[i];
            if (!interaction) {
                continue;
            }
            interaction = Ext.factory(interaction, null, oldMap[interaction.getId && interaction.getId() || interaction.id], 'interaction');
            if (interaction) {
                interaction.setChart(me);
                result.push(interaction);
                result.map[interaction.getId()] = interaction;
            }
        }

        for (i in oldMap) {
            if (!result.map[oldMap[i]]) {
                oldMap[i].destroy();
            }
        }
        return result;
    },

    applyStore: function (store) {
        return Ext.StoreManager.lookup(store);
    },

    updateStore: function (newStore, oldStore) {
        var me = this;
        if (oldStore) {
            oldStore.un('refresh', 'onRefresh', me, null, 'after');
            if (oldStore.autoDestroy) {
                oldStore.destroy();
            }
        }
        if (newStore) {
            newStore.on('refresh', 'onRefresh', me, null, 'after');
        }

        me.fireEvent('storechanged', newStore, oldStore);
        me.onRefresh();
    },

    /**
     * Redraw the chart. If animations are set this will animate the chart too.
     */
    redraw: function () {
        this.fireEvent('redraw', this);
    },

    performLayout: function () {
        this.cancelLayout();
    },

    getEventXY: function (e) {
        e = (e.changedTouches && e.changedTouches[0]) || e.event || e.browserEvent || e;
        var me = this,
            xy = me.element.getXY(),
            region = me.getMainRegion();
        return [e.pageX - xy[0] - region[0], e.pageY - xy[1] - region[1]];
    },

    /**
     * Given an x/y point relative to the chart, find and return the first series item that
     * matches that point.
     * @param {Number} x
     * @param {Number} y
     * @return {Object} An object with `series` and `item` properties, or `false` if no item found.
     */
    getItemForPoint: function (x, y) {
        var me = this,
            i = 0,
            items = me.getSeries(),
            l = items.length,
            series, item;

        for (; i < l; i++) {
            series = items[i];
            item = series.getItemForPoint(x, y);
            if (item) {
                return item;
            }
        }

        return null;
    },

    /**
     * Given an x/y point relative to the chart, find and return all series items that match that point.
     * @param {Number} x
     * @param {Number} y
     * @return {Array} An array of objects with `series` and `item` properties.
     */
    getItemsForPoint: function (x, y) {
        var me = this,
            series = me.getSeries(),
            seriesItem,
            items = [];

        for (var i = 0; i < series.length; i++) {
            seriesItem = series[i];
            var item = seriesItem.getItemForPoint(x, y);
            if (item) {
                items.push(item);
            }
        }

        return items;
    },

    /**
     * @private
     */
    delayThicknessChanged: 0,

    /**
     * @private
     */
    thicknessChanged: false,

    /**
     * Suspend the layout initialized by thickness change
     */
    suspendThicknessChanged: function () {
        this.delayThicknessChanged++;
    },

    /**
     * Resume the layout initialized by thickness change
     */
    resumeThicknessChanged: function () {
        this.delayThicknessChanged--;
        if (this.delayThicknessChanged === 0 && this.thicknessChanged) {
            this.onThicknessChanged();
        }
    },

    onAnimationStart: function () {
        this.fireEvent('animationstart', this);
    },

    onAnimationEnd: function () {
        this.fireEvent('animationend', this);
    },

    onThicknessChanged: function () {
        if (this.delayThicknessChanged === 0) {
            this.thicknessChanged = false;
            this.performLayout();
        } else {
            this.thicknessChanged = true;
        }
    },

    /**
     * @private
     */
    onRefresh: function () {
        var region = this.getMainRegion(),
            axes = this.getAxes(),
            store = this.getStore(),
            series = this.getSeries();
        if (!store || !axes || !series || !region) {
            return;
        }
        this.redraw();
    },

    /**
     * Changes the data store bound to this chart and refreshes it.
     * @param {Ext.data.Store} store The store to bind to this chart.
     */
    bindStore: function (store) {
        this.setStore(store);
    },

    applyHighlightItem: function (newHighlightItem, oldHighlightItem) {
        if (newHighlightItem === oldHighlightItem) {
            return;
        }
        if (Ext.isObject(newHighlightItem) && Ext.isObject(oldHighlightItem)) {
            if (newHighlightItem.sprite === oldHighlightItem.sprite &&
                newHighlightItem.index === oldHighlightItem.index
                ) {
                return;
            }
        }
        return newHighlightItem;
    },

    updateHighlightItem: function (newHighlightItem, oldHighlightItem) {
        if (oldHighlightItem) {
            oldHighlightItem.series.setAttributesForItem(oldHighlightItem, {highlighted: false});
        }
        if (newHighlightItem) {
            newHighlightItem.series.setAttributesForItem(newHighlightItem, {highlighted: true});
        }
    },

    addItemListenersToSeries: function (series) {
        for (var name in this.itemListeners) {
            var listenerMap = this.itemListeners[name], i, ln;
            for (i = 0, ln = listenerMap.length; i < ln; i++) {
                series.addListener.apply(series, listenerMap[i]);
            }
        }
    },

    addItemListener: function (name, fn, scope, options, order) {
        var listenerMap = this.itemListeners[name] || (this.itemListeners[name] = []),
            series = this.getSeries(), seriesItem,
            i, ln;
        listenerMap.push([name, fn, scope, options, order]);
        if (series) {
            for (i = 0, ln = series.length; i < ln; i++) {
                seriesItem = series[i];
                seriesItem.addListener(name, fn, scope, options, order);
            }
        }
    },

    remoteItemListener: function (name, fn, scope, options, order) {
        var listenerMap = this.itemListeners[name],
            series = this.getSeries(), seriesItem,
            i, ln;
        if (listenerMap) {
            for (i = 0, ln = listenerMap.length; i < ln; i++) {
                if (listenerMap[i].fn === fn) {
                    listenerMap.splice(i, 1);
                    if (series) {
                        for (i = 0, ln = series.length; i < ln; i++) {
                            seriesItem = series[i];
                            seriesItem.removeListener(name, fn, scope, options, order);
                        }
                    }
                    break;
                }
            }
        }
    },

    doAddListener: function (name, fn, scope, options, order) {
        if (name.match(this.delegationRegex)) {
            return this.addItemListener(name, fn, scope || this, options, order);
        } else if (name.match(this.domEvents)) {
            return this.element.doAddListener.apply(this.element, arguments);
        } else {
            return this.callSuper(arguments);
        }
    },

    doRemoveListener: function (name, fn, scope, options, order) {
        if (name.match(this.delegationRegex)) {
            return this.remoteItemListener(name, fn, scope || this, options, order);
        } else if (name.match(this.domEvents)) {
            return this.element.doRemoveListener.apply(this.element, arguments);
        } else {
            return this.callSuper(arguments);
        }
    },

    onItemRemove: function (item) {
        this.callSuper(arguments);
        if (this.surfaceMap) {
            Ext.Array.remove(this.surfaceMap[item.type], item);
            if (this.surfaceMap[item.type].length === 0) {
                delete this.surfaceMap[item.type];
            }
        }
    },

    // @private remove gently.
    destroy: function () {
        var me = this,
            emptyArray = [],
            legend = me.getLegend(),
            legendStore = me.getLegendStore();
        me.surfaceMap = null;
        me.setHighlightItem(null);
        me.setSeries(emptyArray);
        me.setAxes(emptyArray);
        me.setInteractions(emptyArray);
        if (legendStore) {
            legendStore.destroy();
            me.legendStore = null;
        }
        if (legend) {
            legend.destroy();
            me.setLegend(null);
        }
        me.setStore(null);
        Ext.Viewport.un('orientationchange', me.redraw, me);
        me.cancelLayout();
        this.callSuper(arguments);
    },

    /* ---------------------------------
     Methods needed for ComponentQuery
     ----------------------------------*/

    /**
     * @private
     * @param {Boolean} deep
     * @return {Array}
     */
    getRefItems: function (deep) {
        var me = this,
            series = me.getSeries(),
            axes = me.getAxes(),
            interaction = me.getInteractions(),
            ans = [], i, ln;

        for (i = 0, ln = series.length; i < ln; i++) {
            ans.push(series[i]);
            if (series[i].getRefItems) {
                ans.push.apply(ans, series[i].getRefItems(deep));
            }
        }

        for (i = 0, ln = axes.length; i < ln; i++) {
            ans.push(axes[i]);
            if (axes[i].getRefItems) {
                ans.push.apply(ans, axes[i].getRefItems(deep));
            }
        }

        for (i = 0, ln = interaction.length; i < ln; i++) {
            ans.push(interaction[i]);
            if (interaction[i].getRefItems) {
                ans.push.apply(ans, interaction[i].getRefItems(deep));
            }
        }

        return ans;
    },

    /**
     * Flattens the given chart surfaces into a single image.
     * Note: Surfaces whose class name is different from chart's engine will be omitted.
     * @param {Array} surfaces A list of chart's surfaces to flatten.
     * @param {String} format If set to 'image', the method will return an Image object. Otherwise, the dataURL
     *                 of the flattened image will be returned.
     * @returns {String|Image} An Image DOM element containing the flattened image or its dataURL.
     */
    flatten: function (surfaces, format) {
        var me = this,
            size = me.element.getSize(),
            svg, canvas, ctx,
            i, surface, region,
            img, dataURL;

        switch (me.engine) {
            case 'Ext.draw.engine.Canvas':
                canvas = document.createElement('canvas');
                canvas.width = size.width;
                canvas.height = size.height;
                ctx = canvas.getContext('2d');
                for (i = 0; i < surfaces.length; i++) {
                    surface = surfaces[i];
                    if (Ext.getClassName(surface) !== me.engine) {
                        continue;
                    }
                    region = surface.getRegion();
                    ctx.drawImage(surface.canvases[0].dom, region[0], region[1]);
                }
                dataURL = canvas.toDataURL();
                break;
            case 'Ext.draw.engine.Svg':
                svg = '<?xml version="1.0" standalone="yes"?>';
                svg += '<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg"' +
                    ' width="' + size.width + '"' +
                    ' height="' + size.height + '">';
                for (i = 0; i < surfaces.length - 1; i++) {
                    surface = surfaces[i];
                    if (Ext.getClassName(surface) !== me.engine) {
                        continue;
                    }
                    region = surface.getRegion();
                    svg += '<g transform="translate(' + region[0] + ',' + region[1] + ')">';
                    svg += Ext.dom.Element.serializeNode(surface.svgElement.dom);
                    svg += '</g>';
                }
                svg += '</svg>';
                dataURL = 'data:image/svg+xml;utf8,' + svg;
                break;
        }
        if (format === 'image') {
            img = new Image();
            img.src = dataURL;
            return img;
        }
        if (format === 'stream') {
            return dataURL.replace(/^data:image\/[^;]+/, 'data:application/octet-stream');
        }
        return dataURL;
    },

    save: function (download) {
        if (download) {
            // TODO: no control over filename, we are at the browser's mercy
            // TODO: unfortunatelly, a.download attribute remains a novelty on mobile: http://caniuse.com/#feat=download
            window.open(this.flatten(this.items.items, 'stream'));
        } else {
            Ext.Viewport.add({
                xtype: 'panel',
                layout: 'fit',
                modal: true,
                width: '90%',
                height: '90%',
                hideOnMaskTap: true,
                centered: true,
                scrollable: false,
                items: {
                    xtype: 'image',
                    mode: 'img',
                    src: this.flatten(this.items.items)
                },
                listeners: {
                    hide: function () {
                        Ext.Viewport.remove(this);
                    }
                }
            }).show();
        }
    }
});