/**
 * @class Ext.chart.interactions.Abstract
 *
 * Defines a common abstract parent class for all interactions.
 *
 */
Ext.define('Ext.chart.interactions.Abstract', {
 
    xtype: 'interaction',
 
    mixins: {
        observable: 'Ext.mixin.Observable'
    },
 
    config: {
 
        /**
         * @cfg {Object} gesture 
         * Maps gestures that should be used for starting/maintaining/ending the interaction
         * to corresponding class methods.
         * @private
         */
 
        gestures: {
            tap: 'onGesture'
        },
 
        /**
         * @cfg {Ext.chart.AbstractChart} chart The chart that the interaction is bound.
         */
        chart: null,
 
        /**
         * @cfg {Boolean} enabled 'true' if the interaction is enabled.
         */
        enabled: true
    },
 
    /**
     * Android device is emerging too many events so if we re-render every frame it will take forever to finish a frame.
     * This throttle technique will limit the timespan between two frames.
     */
    throttleGap: 0,
 
    stopAnimationBeforeSync: false,
 
    constructor: function (config) {
        var me = this;
        me.mixins.observable.constructor.call(me, config);
        me.getId();
        Ext.ComponentManager.register(me);
    },
 
    /**
     * @protected
     * A method to be implemented by subclasses where all event attachment should occur.
     */
    initialize: Ext.emptyFn,
 
    updateChart: function (newChart, oldChart) {
        var me = this;
        if (oldChart === newChart) {
            return;
        }
        if (oldChart) {
            me.removeChartListener(oldChart);
        }
        if (newChart) {
            me.addChartListener();
        }
    },
 
    updateEnabled: function (enabled) {
        var me = this,
            chart = me.getChart();
        if (chart) {
            if (enabled) {
                me.addChartListener();
            } else {
                me.removeChartListener(chart);
            }
        }
    },
 
    /**
     * @protected
     * Placeholder method.
     */
    onGesture: Ext.emptyFn,
 
    /**
     * @protected
     * Find and return a single series item corresponding to the given event,
     * or null if no matching item is found.
     * @param {Event} e 
     * @return {Object} the item object or null if none found.
     */
    getItemForEvent: function (e) {
        var me = this,
            chart = me.getChart(),
            chartXY = chart.getEventXY(e);
        return chart.getItemForPoint(chartXY[0], chartXY[1]);
    },
 
    /**
     * @protected
     * Find and return all series items corresponding to the given event.
     * @param {Event} e 
     * @return {Array} array of matching item objects
     */
    getItemsForEvent: function (e) {
        var me = this,
            chart = me.getChart(),
            chartXY = chart.getEventXY(e);
        return chart.getItemsForPoint(chartXY[0], chartXY[1]);
    },
 
    /**
     * @private
     */
    addChartListener: function () {
        var me = this,
            chart = me.getChart(),
            gestures = me.getGestures(),
            gesture;
 
        if (!me.getEnabled()) {
            return;
        }
 
        function insertGesture(name, fn) {
            chart.addElementListener(
                name,
                // wrap the handler so it does not fire if the event is locked by another interaction 
                me.listeners[name] = function (e) {
                    var locks = me.getLocks(), result;
                    if (me.getEnabled() && (!(name in locks) || locks[name] === me)) {
                        result = (Ext.isFunction(fn) ? fn : me[fn]).apply(this, arguments);
                        if (result === false && e && e.stopPropagation) {
                            e.stopPropagation();
                        }
                        return result;
                    }
                },
                me
            );
        }
 
        me.listeners = me.listeners || {};
        for (gesture in gestures) {
            insertGesture(gesture, gestures[gesture]);
        }
    },
 
    removeChartListener: function (chart) {
        var me = this,
            gestures = me.getGestures(),
            gesture;
 
        function removeGesture(name) {
            chart.removeElementListener(name, me.listeners[name]);
            delete me.listeners[name];
        }
 
        if (me.listeners) {
            for (gesture in gestures) {
                removeGesture(gesture);
            }
        }
    },
 
    lockEvents: function () {
        var me = this,
            locks = me.getLocks(),
            args = Array.prototype.slice.call(arguments),
            i = args.length;
        while (i--) {
            locks[args[i]] = me;
        }
    },
 
    unlockEvents: function () {
        var locks = this.getLocks(),
            args = Array.prototype.slice.call(arguments),
            i = args.length;
        while (i--) {
            delete locks[args[i]];
        }
    },
 
    getLocks: function () {
        var chart = this.getChart();
        return chart.lockedEvents || (chart.lockedEvents = {});
    },
 
    isMultiTouch: function () {
        if (Ext.browser.is.IE10) {
            return true;
        }
        return !(Ext.browser.is.AndroidStock2 || Ext.os.is.Desktop);
    },
 
    initializeDefaults: Ext.emptyFn,
 
    doSync: function () {
        var me = this,
            chart = me.getChart();
        if (me.syncTimer) {
            clearTimeout(me.syncTimer);
            me.syncTimer = null;
        }
        if (me.stopAnimationBeforeSync) {
            ++chart.resizing;
        }
        chart.redraw();
        if (me.stopAnimationBeforeSync) {
            --chart.resizing;
        }
        me.syncThrottle = Date.now() + me.throttleGap;
    },
 
    sync: function () {
        var me = this;
        if (me.throttleGap && Ext.frameStartTime < me.syncThrottle) {
            if (me.syncTimer) {
                return;
            }
            me.syncTimer = Ext.defer(function () {
                me.doSync();
            }, me.throttleGap);
        } else {
            me.doSync();
        }
    },
 
    getItemId: function () {
        return this.getId();
    },
 
    isXType: function (xtype) {
        return xtype === 'interaction';
    },
 
    destroy: function () {
        var me = this,
            chart = me.getChart();
        me.removeChartListener(chart);
        Ext.ComponentManager.unregister(me);
        delete me.listeners;
        me.callParent();
    }
}, function () {
    if (Ext.browser.is.AndroidStock2) {
        this.prototype.throttleGap = 20;
    } else if (Ext.os.is.Android4) {
        this.prototype.throttleGap = 40;
    }
});