/**
 * A wrapper around a DOM element that allows it to receive drops.
 *
 * ## Validity of drag operations
 *
 * There are certain conditions that govern whether a {@link Ext.drag.Source source}
 * and a target can interact. By default (without configuration), all
 * {@link Ext.drag.Source sources} and targets can interact with each other, the conditions
 * are evaluated in this order:
 *
 * ### {@link #isDisabled Disabled State}
 * If the target is disabled, the {@link Ext.drag.Source source} 
 * cannot interact with it.
 *
 * ### {@link #groups Groups}
 * Both the {@link Ext.drag.Source source} and target can belong to multiple groups. 
 * They may interact if:
 * - Neither has a group
 * - Both have one (or more) of the same group
 *
 * ### {@link #method!accepts Accept}
 * This method is called each time a {@link Ext.drag.Source source} enters this
 * target. If the method returns `false`, the drag is not considered valid.
 *
 * ## Asynchronous drop processing
 *
 *  When the drop completes, the {@link #drop} event will fire, however the underlying data
 * may not be ready to be consumed. By returning a {@link Ext.Promise Promise} from the data, 
 * it allows either:
 * - The data to be fetched (either from a remote source or generated if expensive).
 * - Any validation to take place before the drop is finalized.
 *
 * Once the promise is {@link Ext.Promise#resolve resolved} or {@link Ext.Promise#resolve rejected},
 * further processing can be completed.
 *
 * Validation example:
 *
 * 
 *      var confirmSource = new Ext.drag.Source({
 *          element: dragEl,
 *          describe: function(info) {
 *              // Provide the data up front
 *              info.setData('records', theRecords);
 *          }
 *      });  
 *
 *      var confirmTarget = new Ext.drag.Target({
 *          element: dropEl,
 *          listeners: {
 *              drop: function(target, info) {
 *                  Ext.MessageBox.confirm('Really', 'Are you sure?', function(btn) {
 *                      if (btn === 'yes') {
 *                          info.getData('records').then(function(data) {
 *                              // Process the data
 *                          });
 *                      }
 *                  });
 *              }
 *          }
 *      });
 *
 *
 * Remote data example:
 *
 *      var fetchSource = new Ext.drag.Source({
 *          element: dragEl,
 *          // The resulting drag data will be a binary blob
 *          // of image data, we don't want to fetch it up front, so
 *          // pass a callback to be executed when data is requested.
 *          describe: function(info) {
 *              info.setData('image', function() {
 *                  return Ext.Ajax.request({
 *                      url: 'data.json'
 *                      // some options
 *                  }).then(function(result) {
 *                      var imageData;
 *                      // Do some post-processing
 *                      return imageData;
 *                  }, function() {
 *                      return Ext.Promise.reject('Something went wrong!');
 *                  });
 *              });
 *          }
 *      });
 *
 *      var fetchTarget = new Ext.drag.Target({
 *          element: dropEl,
 *          accepts: function(info) {
 *              return info.types.indexOf('image') > -1;
 *          },
 *          listeners: {
 *              drop: function(target, info) {
 *                  info.getData('image').then(function() {
 *                      // All good, show the image
 *                  }, function() {
 *                      // Handle failure case
 *                  });
 *              }
 *          }
 *      });
 * 
 */
Ext.define('Ext.drag.Target', {
    extend: 'Ext.drag.Item',
 
    requires: ['Ext.drag.Manager'],
 
    defaultIdPrefix: 'target-',
 
    config: {
        /**
         * @cfg {String} invalidCls
         * A class to add to the {@link #element} when an
         * invalid drag is over this target.
         */
        invalidCls: '',
 
        /**
         * @cfg {String} validCls
         * A class to add to the {@link #element} when an
         * invalid drag is over this target.
         */
        validCls: ''
    },
 
    /**
     * @cfg {Function} accepts
     * See {@link #method-accepts}.
     */
    
    /**
     * @event beforedrop
     * Fires before a valid drop occurs. Return `false` to prevent the drop from
     * completing.
     *
     * @param {Ext.drag.Target} this This target.
     * @param {Ext.drag.Info} info The drag info.
     */
    
    /**
     * @event drop
     * Fires when a valid drop occurs.
     *
     * @param {Ext.drag.Target} this This target.
     * @param {Ext.drag.Info} info The drag info.
     */
    
    /**
     * @event dragenter
     * Fires when a drag enters this target.
     *
     * @param {Ext.drag.Target} this This target.
     * @param {Ext.drag.Info} info The drag info.
     */ 
    
    /**
     * @event dragleave
     * Fires when a source leaves this target.
     *
     * @param {Ext.drag.Target} this This target.
     * @param {Ext.drag.Info} info The drag info.
     */ 
    
    /**
     * @event dragmove
     * Fires when a drag moves while inside this target.
     *
     * @param {Ext.drag.Target} this This target.
     * @param {Ext.drag.Info} info The drag info.
     */ 
 
    constructor: function(config) {
        var me = this,
            accepts = config && config.accepts;
 
        if (accepts) {
            me.accepts = accepts;
            // Don't mutate the object the user passed. Need to do this
            // here otherwise initConfig will complain about writing over
            // the method.
            config = Ext.apply({}, config);
            delete config.accepts;
        }
        
        me.callParent([config]);
        
        Ext.drag.Manager.register(me);
    },
 
    /**
     * Called each time a {@link Ext.drag.Source source} enters this target.
     * Allows this target to indicate whether it will interact with
     * the given drag. Determined after {@link #isDisabled} and 
     * {@link #groups} checks. If either of the aforementioned conditions
     * means the target is not valid, this will not be called.
     *
     * Defaults to returning `true`.
     * 
     * @param {Ext.drag.Info} info The drag info.
     * @return {Boolean} `true` if the drag is valid for this target.
     *
     * @protected
     */
    accepts: function(info) {
        return true;
    },
 
    /**
     * @method disable
     * @inheritdoc
     */
    disable: function() {
        this.callParent();
        this.setupListeners(null);
    },
 
    /**
     * @method enable
     * @inheritdoc
     */
    enable: function() {
        this.callParent();
        this.setupListeners();
    },
 
    /**
     * @method
     * Called before a drag finishes on this target. Return `false` to veto
     * the drop.
     * @param {Ext.drag.Info} info The drag info.
     * @return {Boolean} `false` to veto the drop.
     *
     * @protected
     * @template
     */
    beforeDrop: Ext.emptyFn,
 
    /**
     * @method
     * Called when a drag is dropped on this target.
     * @param {Ext.drag.Info} info The drag info.
     *
     * @protected
     * @template
     */
    onDrop: Ext.emptyFn,
 
    /**
     * @method
     * Called when a drag enters this target.
     * @param {Ext.drag.Info} info The drag info.
     *
     * @protected
     * @template
     */
    onDragEnter: Ext.emptyFn,
 
    /**
     * @method
     * Called when a source leaves this target.
     * @param {Ext.drag.Info} info The drag info.
     *
     * @protected
     * @template
     */
    onDragLeave: Ext.emptyFn,
 
    /**
     * @method
     * Called when a drag is moved while inside this target.
     * @param {Ext.drag.Info} info The drag info.
     *
     * @protected
     * @template
     */
    onDragMove: Ext.emptyFn,
 
    updateInvalidCls: function(invalidCls, oldInvalidCls) {
        var info = this.info;
        
        this.doUpdateCls(info && !info.valid, invalidCls, oldInvalidCls);
    },
 
    updateValidCls: function(validCls, oldValidCls) {
        var info = this.info;
        
        this.doUpdateCls(info && info.valid, validCls, oldValidCls);
    },
 
    destroy: function() {
        Ext.drag.Manager.unregister(this);
        
        this.callParent();
    },
 
    privates: {
        /**
         * Removes a class and replaces it with a new one, if the old class
         * was already on the element.
         *
         * @param {Boolean} needsAdd `true` if the new class needs adding.
         * @param {String} cls The new class to add.
         * @param {String} oldCls The old class to remove.
         *
         * @private
         */
        doUpdateCls: function(needsAdd, cls, oldCls) {
            var el = this.getElement();
 
            if (oldCls) {
                el.removeCls(oldCls);
            }
 
            if (cls && needsAdd) {
                el.addCls(cls);
            }
        },
 
        /**
         * @method getElListeners
         * @inheritdoc
         */
        getElListeners: function() {
            return {
                dragenter: 'handleNativeDragEnter',
                dragleave: 'handleNativeDragLeave',
                dragover: 'handleNativeDragMove',
                drop: 'handleNativeDrop'
            };
        },
 
        /**
         * Called when a drag is dropped on this target.
         * @param {Ext.drag.Info} info The drag info.
         *
         * @private
         */
        handleDrop: function(info) {
            var me = this,
                hasListeners = me.hasListeners,
                valid = info.valid;
 
            me.getElement().removeCls([me.getInvalidCls(), me.getValidCls()]);
 
            if (valid && me.beforeDrop(info) !== false) {
                if (hasListeners.beforedrop && me.fireEvent('beforedrop', me, info) === false) {
                    return false;
                }
                
                me.onDrop(info);
                
                if (hasListeners.drop) {
                    me.fireEvent('drop', me, info);
                }
            }
            else {
                return false;
            }
        },
 
        /**
         * Called when a drag enters this target.
         * @param {Ext.drag.Info} info The drag info.
         *
         * @private
         */
        handleDragEnter: function(info) {
            var me = this,
                cls = info.valid ? me.getValidCls() : me.getInvalidCls();
 
            if (cls) {
                me.getElement().addCls(cls);
            }
 
            me.onDragEnter(info);
            
            if (me.hasListeners.dragenter) {
                me.fireEvent('dragenter', me, info);
            }
        },
 
        /**
         * Called when a source leaves this target.
         * @param {Ext.drag.Info} info The drag info.
         *
         * @private
         */
        handleDragLeave: function(info) {
            var me = this;
 
            me.getElement().removeCls([me.getInvalidCls(), me.getValidCls()]);
            me.onDragLeave(info);
            
            if (me.hasListeners.dragleave) {
                me.fireEvent('dragleave', me, info);
            }
        },
 
        /**
         * Called when a drag is moved while inside this target.
         * @param {Ext.drag.Info} info The drag info.
         *
         * @private
         */
        handleDragMove: function(info) {
            var me = this;
            
            me.onDragMove(info);
            
            if (me.hasListeners.dragmove) {
                me.fireEvent('dragmove', me, info);
            }
        },
 
        /**
         * Handle a native drag enter.
         * @param {Ext.event.Event} e The event.
         * 
         * @private
         */
        handleNativeDragEnter: function(e) {
            var me = this,
                info = Ext.drag.Manager.getNativeDragInfo(e);
 
            info.onNativeDragEnter(me, e);
 
            if (me.hasListeners.dragenter) {
                me.fireEvent('dragenter', me, info);
            }
        },
 
        /**
         * Handle a native drag leave.
         * @param {Ext.event.Event} e The event.
         * 
         * @private
         */
        handleNativeDragLeave: function(e) {
            var me = this,
                info = Ext.drag.Manager.getNativeDragInfo(e);
 
            info.onNativeDragLeave(me, e);
            
            if (me.hasListeners.dragleave) {
                me.fireEvent('dragleave', me, info);
            }
        },
 
        /**
         * Handle a native drag move.
         * @param {Ext.event.Event} e The event.
         * 
         * @private
         */
        handleNativeDragMove: function(e) {
            var me = this,
                info = Ext.drag.Manager.getNativeDragInfo(e);
 
            info.onNativeDragMove(me, e);
 
            if (me.hasListeners.dragmove) {
                me.fireEvent('dragmove', me, info);
            }
        },
 
        /**
         * Handle a native drop.
         * @param {Ext.event.Event} e The event.
         * 
         * @private
         */
        handleNativeDrop: function(e) {
            var me = this,
                hasListeners = me.hasListeners,
                info = Ext.drag.Manager.getNativeDragInfo(e),
                valid = info.valid;
 
            info.onNativeDrop(me, e);
 
            if (valid) {
                if (hasListeners.beforedrop && me.fireEvent('beforedrop', me, info) === false) {
                    return;
                }
                
                if (hasListeners.drop) {
                    me.fireEvent('drop', me, info);
                }
            }
        }
    }
});