/**
 * Defines a common abstract parent class for all D3 interactions.
 */
Ext.define('Ext.d3.interaction.Abstract', {

    isInteraction: true,

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

    statics: {
        /**
         * @private
         * Map of components to locked events on those components, e.g.:
         *
         *     {
         *         componentId: {
         *             drag: true,
         *             pinch: true
         *         }
         *     }
         */
        lockedEvents: {}
    },

    config: {
        /**
         * @cfg {Ext.d3.Component} component
         * The interaction will listen for gestures on this component's element.
         */
        component: null,

        /**
         * @cfg {Object} gestures
         * Maps gestures that should be used for starting/maintaining/ending
         * the interaction to corresponding class methods. For example:
         *
         *     gestures: {
         *         tap: 'onGesture'
         *     }
         *
         * It is also possible to override the default getter for the `gestures`
         * config, that will derive gestures to be used based on other configs' values.
         * For example, a subclass can define:
         *
         *     getGestures: function () {
         *         var someConfig = this.getSomeConfig(),
         *             gestures = {};
         *
         *         gestures[someConfig.gesture] = 'onGesture';
         *
         *         return gestures;
         *     }
         *
         */
        gestures: null,

        /**
         * @cfg {Boolean} [enabled=true] `true` if the interaction is enabled.
         */
        enabled: true
    },

    /**
     * @private
     * @event destroy
     * Fires when an interaction is destroyed.
     * @param {Ext.d3.interaction.Abstract} interaction
     */

    /**
     * @private
     * Class names or namespaces of supported components, e.g.:
     * Ext.d3
     * Ext.d3.hierarchy.Pack
     */
    supports: [],

    listeners: null,

    constructor: function(config) {
        var me = this,
            id;

        config = config || {};

        if ('id' in config) {
            id = config.id;
        }
        else if ('id' in me.config) {
            id = me.config.id;
        }
        else {
            id = me.getId();
        }

        me.setId(id);

        me.mixins.observable.constructor.call(me, config);
    },

    resetComponent: function() {
        var me = this,
            component = me.getComponent();

        if (!me.isConfiguring) {
            me.setComponent(null);
            me.setGestures(null);
            me.setComponent(component);
        }
    },

    updateComponent: function(component, oldComponent) {
        var me = this;

        if (oldComponent === component) {
            return;
        }

        if (oldComponent) {
            me.removeComponent(oldComponent);
        }

        if (component) {
            me.addComponent(component);
        }
    },

    addComponent: function(component) {
        component.register(this);
        this.component = component;
        this.addElementListener(component.element);
    },

    removeComponent: function(component) {
        this.removeElementListener(component.element);
        this.component = null;
        component.unregister(this);
    },

    updateEnabled: function(enabled) {
        var me = this,
            component = me.getComponent();

        if (component) {
            if (enabled) {
                me.addElementListener(component.element);
            }
            else {
                me.removeElementListener(component.element);
            }
        }
    },

    /**
     * @method
     * @protected
     * Placeholder method.
     */
    onGesture: Ext.emptyFn,

    /**
     * @private
     */
    addElementListener: function(element) {
        var me = this,
            gestures = me.getGestures(),
            locks = me.getLocks(),
            name;

        if (!me.getEnabled()) {
            return;
        }

        function addGesture(name, fn) {
            if (!Ext.isFunction(fn)) {
                fn = me[fn];
            }

            element.on(
                name,
                me.listeners[name] = function(e) {
                    var result;

                    if (me.getEnabled() && (!(name in locks) || locks[name] === me)) {
                        result = fn.apply(this, arguments);

                        if (result === false && e && e.stopPropagation) {
                            e.stopPropagation();
                        }

                        return result;
                    }
                },
                me
            );
        }

        me.listeners = me.listeners || {};

        for (name in gestures) {
            addGesture(name, gestures[name]);
        }
    },

    removeElementListener: function(element) {
        var me = this,
            gestures = me.getGestures(),
            name;

        function removeGesture(name) {
            var fn = me.listeners[name];

            if (fn) {
                element.un(name, fn);
                delete me.listeners[name];
            }
        }

        if (me.listeners) {
            for (name in gestures) {
                removeGesture(name);
            }
        }
    },

    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() {
        return this.statics().lockedEvents;
    },

    destroy: function() {
        var me = this;

        me.fireEvent('destroy', me);
        me.setComponent(null);
        me.listeners = null;
    }

});
