/* eslint-disable no-empty-function */
/**
 * The core drag and drop module for Moodle which extends the YUI drag and
 * drop functionality with additional features.
 *
 * @module moodle-core-dragdrop
 */
var MOVEICON = {
    pix: "i/move_2d",
    largepix: "i/dragdrop",
    component: 'moodle',
    cssclass: 'moodle-core-dragdrop-draghandle'
};
/**
 * General DRAGDROP class, this should not be used directly,
 * it is supposed to be extended by your class
 *
 * @class M.core.dragdrop
 * @constructor
 * @extends Base
 */
var DRAGDROP = function() {
    DRAGDROP.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGDROP, Y.Base, {
    /**
     * Whether the item is being moved upwards compared with the last
     * location.
     *
     * @property goingup
     * @type Boolean
     * @default null
     */
    goingup: null,
    /**
     * Whether the item is being moved upwards compared with the start
     * point.
     *
     * @property absgoingup
     * @type Boolean
     * @default null
     */
    absgoingup: null,
    /**
     * The class for the object.
     *
     * @property samenodeclass
     * @type String
     * @default null
     */
    samenodeclass: null,
    /**
     * The class on the parent of the item being moved.
     *
     * @property parentnodeclass
     * @type String
     * @default
     */
    parentnodeclass: null,
    /**
     * The label to use with keyboard drag/drop to describe items of the same Node.
     *
     * @property samenodelabel
     * @type Object
     * @default null
     */
    samenodelabel: null,
    /**
     * The label to use with keyboard drag/drop to describe items of the parent Node.
     *
     * @property samenodelabel
     * @type Object
     * @default null
     */
    parentnodelabel: null,
    /**
     * The groups for this instance.
     *
     * @property groups
     * @type Array
     * @default []
     */
    groups: [],
    /**
     * The previous drop location.
     *
     * @property lastdroptarget
     * @type Node
     * @default null
     */
    lastdroptarget: null,
    /**
     * Listeners.
     *
     * @property listeners
     * @type Array
     * @default null
     */
    listeners: null,
    /**
     * The initializer which sets up the move action.
     *
     * @method initializer
     * @protected
     */
    initializer: function() {
        this.listeners = [];
        // Listen for all drag:start events.
        this.listeners.push(Y.DD.DDM.on('drag:start', this.global_drag_start, this));
        // Listen for all drag:end events.
        this.listeners.push(Y.DD.DDM.on('drag:end', this.global_drag_end, this));
        // Listen for all drag:drag events.
        this.listeners.push(Y.DD.DDM.on('drag:drag', this.global_drag_drag, this));
        // Listen for all drop:over events.
        this.listeners.push(Y.DD.DDM.on('drop:over', this.global_drop_over, this));
        // Listen for all drop:hit events.
        this.listeners.push(Y.DD.DDM.on('drop:hit', this.global_drop_hit, this));
        // Listen for all drop:miss events.
        this.listeners.push(Y.DD.DDM.on('drag:dropmiss', this.global_drag_dropmiss, this));
        // Add keybaord listeners for accessible drag/drop
        this.listeners.push(Y.one(Y.config.doc.body).delegate('key', this.global_keydown,
                'down:32, enter, esc', '.' + MOVEICON.cssclass, this));
        // Make the accessible drag/drop respond to a single click.
        this.listeners.push(Y.one(Y.config.doc.body).delegate('click', this.global_keydown,
                '.' + MOVEICON.cssclass, this));
    },
    /**
     * The destructor to shut down the instance of the dragdrop system.
     *
     * @method destructor
     * @protected
     */
    destructor: function() {
        new Y.EventHandle(this.listeners).detach();
    },
    /**
     * Build a new drag handle Node.
     *
     * @method get_drag_handle
     * @param {String} title The title on the drag handle
     * @param {String} classname The name of the class to add to the node
     * wrapping the drag icon
     * @param {String} [iconclass] The class to add to the icon
     * @param {Boolean} [large=false] whether to use the larger version of
     * the drag icon
     * @return Node The built drag handle.
     */
    get_drag_handle: function(title, classname, iconclass, large) {
        var iconname = MOVEICON.pix;
        if (large) {
            iconname = MOVEICON.largepix;
        }
        var dragicon = Y.Node.create('')
            .setStyle('cursor', 'move')
            .setAttrs({
                'src': M.util.image_url(iconname, MOVEICON.component),
                'alt': title
            });
        if (iconclass) {
            dragicon.addClass(iconclass);
        }
        var dragelement = Y.Node.create('')
            .addClass(classname)
            .setAttribute('title', title)
            .setAttribute('tabIndex', 0)
            .setAttribute('data-draggroups', this.groups)
            .setAttribute('role', 'button');
        dragelement.appendChild(dragicon);
        dragelement.addClass(MOVEICON.cssclass);
        return dragelement;
    },
    lock_drag_handle: function(drag, classname) {
        drag.removeHandle('.' + classname);
    },
    unlock_drag_handle: function(drag, classname) {
        drag.addHandle('.' + classname);
        drag.get('activeHandle').focus();
    },
    ajax_failure: function(response) {
        var e = {
            name: response.status + ' ' + response.statusText,
            message: response.responseText
        };
        return new M.core.exception(e);
    },
    in_group: function(target) {
        var ret = false;
        Y.each(this.groups, function(v) {
            if (target._groups[v]) {
                ret = true;
            }
        }, this);
        return ret;
    },
    /*
        * Drag-dropping related functions
        */
    global_drag_start: function(e) {
        // Get our drag object
        var drag = e.target;
        // Check that drag object belongs to correct group
        if (!this.in_group(drag)) {
            return;
        }
        // Store the nodes current style, so we can restore it later.
        this.originalstyle = drag.get('node').getAttribute('style');
        // Set some general styles here
        drag.get('node').setStyle('opacity', '.25');
        drag.get('dragNode').setStyles({
            opacity: '.75',
            borderColor: drag.get('node').getStyle('borderColor'),
            backgroundColor: drag.get('node').getStyle('backgroundColor')
        });
        drag.get('dragNode').empty();
        this.drag_start(e);
    },
    global_drag_end: function(e) {
        var drag = e.target;
        // Check that drag object belongs to correct group
        if (!this.in_group(drag)) {
            return;
        }
        // Put our general styles back
        drag.get('node').setAttribute('style', this.originalstyle);
        this.drag_end(e);
    },
    global_drag_drag: function(e) {
        var drag = e.target,
            info = e.info;
        // Check that drag object belongs to correct group
        if (!this.in_group(drag)) {
            return;
        }
        // Note, we test both < and > situations here. We don't want to
        // effect a change in direction if the user is only moving side
        // to side with no Y position change.
        // Detect changes in the position relative to the start point.
        if (info.start[1] < info.xy[1]) {
            // We are going up if our final position is higher than our start position.
            this.absgoingup = true;
        } else if (info.start[1] > info.xy[1]) {
            // Otherwise we're going down.
            this.absgoingup = false;
        }
        // Detect changes in the position relative to the last movement.
        if (info.delta[1] < 0) {
            // We are going up if our final position is higher than our start position.
            this.goingup = true;
        } else if (info.delta[1] > 0) {
            // Otherwise we're going down.
            this.goingup = false;
        }
        this.drag_drag(e);
    },
    global_drop_over: function(e) {
        // Check that drop object belong to correct group.
        if (!e.drop || !e.drop.inGroup(this.groups)) {
            return;
        }
        // Get a reference to our drag and drop nodes.
        var drag = e.drag.get('node'),
            drop = e.drop.get('node');
        // Save last drop target for the case of missed target processing.
        this.lastdroptarget = e.drop;
        // Are we dropping within the same parent node?
        if (drop.hasClass(this.samenodeclass)) {
            var where;
            if (this.goingup) {
                where = "before";
            } else {
                where = "after";
            }
            // Add the node contents so that it's moved, otherwise only the drag handle is moved.
            drop.insert(drag, where);
        } else if ((drop.hasClass(this.parentnodeclass) || drop.test('[data-droptarget="1"]')) && !drop.contains(drag)) {
            // We are dropping on parent node and it is empty
            if (this.goingup) {
                drop.append(drag);
            } else {
                drop.prepend(drag);
            }
        }
        this.drop_over(e);
    },
    global_drag_dropmiss: function(e) {
        // drag:dropmiss does not have e.drag and e.drop properties
        // we substitute them for the ease of use. For e.drop we use,
        // this.lastdroptarget (ghost node we use for indicating where to drop)
        e.drag = e.target;
        e.drop = this.lastdroptarget;
        // Check that drag object belongs to correct group
        if (!this.in_group(e.drag)) {
            return;
        }
        // Check that drop object belong to correct group
        if (!e.drop || !e.drop.inGroup(this.groups)) {
            return;
        }
        this.drag_dropmiss(e);
    },
    global_drop_hit: function(e) {
        // Check that drop object belong to correct group
        if (!e.drop || !e.drop.inGroup(this.groups)) {
            return;
        }
        this.drop_hit(e);
    },
    /**
     * This is used to build the text for the heading of the keyboard
     * drag drop menu and the text for the nodes in the list.
     * @method find_element_text
     * @param {Node} n The node to start searching for a valid text node.
     * @return {string} The text of the first text-like child node of n.
     */
    find_element_text: function(n) {
        // The valid node types to get text from.
        var nodes = n.all('h2, h3, h4, h5, span:not(.actions):not(.menu-action-text), p, div.no-overflow, div.dimmed_text');
        var text = '';
        nodes.each(function() {
            if (text === '') {
                if (Y.Lang.trim(this.get('text')) !== '') {
                    text = this.get('text');
                }
            }
        });
        if (text !== '') {
            return text;
        }
        return M.util.get_string('emptydragdropregion', 'moodle');
    },
    /**
     * This is used to initiate a keyboard version of a drag and drop.
     * A dialog will open listing all the valid drop targets that can be selected
     * using tab, tab, tab, enter.
     * @method global_start_keyboard_drag
     * @param {Event} e The keydown / click event on the grab handle.
     * @param {Node} dragcontainer The resolved draggable node (an ancestor of the drag handle).
     * @param {Node} draghandle The node that triggered this action.
     */
    global_start_keyboard_drag: function(e, draghandle, dragcontainer) {
        M.core.dragdrop.keydragcontainer = dragcontainer;
        M.core.dragdrop.keydraghandle = draghandle;
        // Get the name of the thing to move.
        var nodetitle = this.find_element_text(dragcontainer);
        var dialogtitle = M.util.get_string('movecontent', 'moodle', nodetitle);
        // Build the list of drop targets.
        var droplist = Y.Node.create('