/*
 * Sortable plugin.
 *
 * Status: experimental. The behavior is not perfect, but it's OK in terms of memory
 * usage and disposing.
 *
 * This is a lightweight, Winter-style implementation of the drag & drop sorting
 * functionality. The plugin uses only HTML5 Drag&Drop feature and completely
 * disposable.
 *
 * During the dragging the plugin creates a placeholder element, which should be
 * styled separately.
 *
 * Draggable elements should be marked with "draggable" HTML5 attribute.
 *
 * Current / planned features:
 *
 * [x] Sorting a single list.
 * [ ] Dragging items between multiple lists.
 * [ ] Sorting nested lists.

 * JAVASCRIPT API
 *
 * $('#list').listSortable({})
 *
 * DATA ATTRIBUTES API
 *
 * In the simplest case the plugin can be initialized like this:
 * <ul data-control="list-sortable">
 *     <li draggable="true">...</li>
 *
 * Multiple lists will not support this option and the plugin should be created
 * and updated by a caller code.
 *
 * Options:
 * - handle: optional selector for a drag handle element. Also available as data-handle attribute.
 * - direction: direction of the list - horizontal or vertical. Also available as data-direction attribute. Default is vertical.
 *
 * Events:
 * - dragged.list.sortable - triggered on a list element after it was moved
 */

+function ($) { "use strict";

    var Base = $.wn.foundation.base,
        BaseProto = Base.prototype,
        listSortableIdCounter = 0,
        elementsIdCounter = 0

    var ListSortable = function (element, options) {
        this.lists = []
        this.options = options
        this.listSortableId = null
        this.lastMousePosition = null

        Base.call(this)

        $.wn.foundation.controlUtils.markDisposable(element)
        this.init()

        this.addList(element)
    }

    ListSortable.prototype = Object.create(BaseProto)
    ListSortable.prototype.constructor = ListSortable

    ListSortable.prototype.init = function () {
        listSortableIdCounter++

        this.listSortableId = 'listsortable/id/' + listSortableIdCounter
    }

    ListSortable.prototype.addList = function(list) {
        this.lists.push(list)
        this.registerListHandlers(list)

        if (this.lists.length == 1) {
            $(list).one('dispose-control', this.proxy(this.dispose))
        }
    }

    //
    // Event management
    //

    ListSortable.prototype.registerListHandlers = function(list) {
        var $list = $(list)

        $list.on('dragstart', '> li', this.proxy(this.onDragStart))
        $list.on('dragover', '> li', this.proxy(this.onDragOver))
        $list.on('dragenter', '> li', this.proxy(this.onDragEnter))
        $list.on('dragleave', '> li', this.proxy(this.onDragLeave))
        $list.on('drop', '> li', this.proxy(this.onDragDrop))
        $list.on('dragend', '> li', this.proxy(this.onDragEnd))
    }

    ListSortable.prototype.unregisterListHandlers = function(list) {
        var $list = $(list)

        $list.off('dragstart', '> li', this.proxy(this.onDragStart))
        $list.off('dragover', '> li', this.proxy(this.onDragOver))
        $list.off('dragenter', '> li', this.proxy(this.onDragEnter))
        $list.off('dragleave', '> li', this.proxy(this.onDragLeave))
        $list.off('drop', '> li', this.proxy(this.onDragDrop))
        $list.off('dragend', '> li', this.proxy(this.onDragEnd))
    }

    ListSortable.prototype.unregisterHandlers = function() {
        $(document).off('dragover', this.proxy(this.onDocumentDragOver))
        $(document).off('mousemove', this.proxy(this.onDocumentMouseMove))
        $(this.lists[0]).off('dispose-control', this.proxy(this.dispose))
    }

    //
    // Disposing
    //

    ListSortable.prototype.unbindLists = function() {
        for (var i=this.lists.length-1; i>0; i--) {
            var list = this.lists[i]

            this.unregisterListHandlers(this.lists[i])
            $(list).removeData('oc.listSortable')
        }
    }

    ListSortable.prototype.dispose = function() {
        this.unbindLists()
        this.unregisterHandlers()

        this.options = null
        this.lists = []

        BaseProto.dispose.call(this)
    }

    //
    // Internal helpers
    //

    ListSortable.prototype.elementBelongsToManagedList = function(element) {
        for (var i=this.lists.length-1; i >= 0; i--) {
            var list = this.lists[i],
                children = [].slice.call(list.children); // Converts HTMLCollection to array

            if (children.indexOf(element) !== -1) {
                return true
            }
        }

        return false
    }

    ListSortable.prototype.isDragStartAllowed = function(element) {
        // TODO: if handle selector is specified - test if
        // the element is a handle.

        return true
    }

    ListSortable.prototype.elementIsPlaceholder = function(element) {
        return element.getAttribute('class') === 'list-sortable-placeholder'
    }

    ListSortable.prototype.getElementSortableId = function(element) {
        if (element.hasAttribute('data-list-sortable-element-id')) {
            return element.getAttribute('data-list-sortable-element-id')
        }

        elementsIdCounter++
        var elementId = elementsIdCounter

        element.setAttribute('data-list-sortable-element-id', elementsIdCounter)

        return elementsIdCounter
    }

    ListSortable.prototype.dataTransferContains = function(ev, element) {
        if (ev.dataTransfer.types.indexOf !== undefined){
            return ev.dataTransfer.types.indexOf(element) >= 0
        }

        return ev.dataTransfer.types.contains(element)
    }

    ListSortable.prototype.isSourceManagedList = function(ev) {
        return this.dataTransferContains(ev, this.listSortableId)
    }

    ListSortable.prototype.removePlaceholders = function() {
        for (var i=this.lists.length-1; i >= 0; i--) {
            var list = this.lists[i],
                placeholders = list.querySelectorAll('.list-sortable-placeholder')

            for (var j=placeholders.length-1; j >= 0; j--) {
                list.removeChild(placeholders[j])
            }
        }
    }

    ListSortable.prototype.createPlaceholder = function(element, ev) {
        var placeholder = document.createElement('li'),
            placement = this.getPlaceholderPlacement(element, ev)

        this.removePlaceholders()

        placeholder.setAttribute('class', 'list-sortable-placeholder')
        placeholder.setAttribute('draggable', true)

        if (placement == 'before') {
            element.parentNode.insertBefore(placeholder, element)
        }
        else {
            element.parentNode.insertBefore(placeholder, element.nextSibling)
        }
    }

    ListSortable.prototype.moveElement = function(target, ev) {
        var list = target.parentNode,
            placeholder = list.querySelector('.list-sortable-placeholder')

        if (!placeholder) {
            return
        }

        var elementId = ev.dataTransfer.getData('listsortable/elementid')
        if (!elementId) {
            return
        }

        var item = this.findDraggedItem(elementId)
        if (!item) {
            return
        }

        placeholder.parentNode.insertBefore(item, placeholder)
        $(item).trigger('dragged.list.sortable')
    }

    ListSortable.prototype.findDraggedItem = function(elementId) {
        for (var i=this.lists.length-1; i >= 0; i--) {
            var list = this.lists[i],
                item = list.querySelector('[data-list-sortable-element-id="'+elementId+'"]')

            if (item) {
                return item
            }
        }

        return null
    }

    ListSortable.prototype.getPlaceholderPlacement = function(hoverElement, ev) {
        var mousePosition = $.wn.foundation.event.pageCoordinates(ev),
            elementPosition = $.wn.foundation.element.absolutePosition(hoverElement)

        if (this.options.direction == 'vertical') {
            var elementCenter = elementPosition.top + hoverElement.offsetHeight/2

            return mousePosition.y <= elementCenter ? 'before' : 'after'
        }
        else {
            var elementCenter = elementPosition.left + hoverElement.offsetWidth/2

            return mousePosition.x <= elementCenter ? 'before' : 'after'
        }
    }

    ListSortable.prototype.lastMousePositionChanged = function(ev) {
        var mousePosition = $.wn.foundation.event.pageCoordinates(ev.originalEvent)

        if (this.lastMousePosition === null || this.lastMousePosition.x != mousePosition.x || this.lastMousePosition.y != mousePosition.y) {
            this.lastMousePosition = mousePosition
            return true
        }

        return false
    }

    ListSortable.prototype.mouseOutsideLists = function(ev) {
        var mousePosition = $.wn.foundation.event.pageCoordinates(ev)

        for (var i=this.lists.length-1; i >= 0; i--) {
            if ($.wn.foundation.element.elementContainsPoint(this.lists[i], mousePosition)) {
                return false
            }
        }

        return true
    }

    ListSortable.prototype.getClosestDraggableParent = function(element) {
        var current = element

        while (current) {
            if (current.tagName === 'LI' && current.hasAttribute('draggable') ) {
                return current
            }

            current = current.parentNode
        }

        return null
    }

    // EVENT HANDLERS
    // ============================

    ListSortable.prototype.onDragStart = function(ev) {
        if (!this.isDragStartAllowed(ev.target)) {
            return
        }

        ev.originalEvent.dataTransfer.effectAllowed = 'move'
        ev.originalEvent.dataTransfer.setData('listsortable/elementid', this.getElementSortableId(ev.target))
        ev.originalEvent.dataTransfer.setData(this.listSortableId, this.listSortableId)

        // The mousemove handler is used to remove the placeholder
        // when the drag is canceled with Escape button. We can't use
        // the dragend for removing the placeholders because dragend
        // is triggered before drop, but we need placeholder to exists
        // in the drop handler.
        //
        // Mouse events are suppressed during the drag and drop operations,
        // so we only need to handle it once (but we still must the handler
        // explicitly).
        $(document).on('mousemove', this.proxy(this.onDocumentMouseMove))

        // The dragover handler is used to hide the placeholder when
        // the mouse is outside of any known list.
        $(document).on('dragover', this.proxy(this.onDocumentDragOver))
    }

    ListSortable.prototype.onDragOver = function(ev) {
        if (!this.isSourceManagedList(ev.originalEvent)) {
            return
        }

        var draggable = this.getClosestDraggableParent(ev.target)
        if (!draggable) {
            return
        }

        if (!this.elementIsPlaceholder(draggable) && this.lastMousePositionChanged(ev)) {
            this.createPlaceholder(draggable, ev.originalEvent)
        }

        ev.stopPropagation()
        ev.preventDefault()
        ev.originalEvent.dataTransfer.dropEffect = 'move'
    }

    ListSortable.prototype.onDragEnter = function(ev) {
        if (!this.isSourceManagedList(ev.originalEvent)) {
            return
        }

        var draggable = this.getClosestDraggableParent(ev.target)
        if (!draggable) {
            return
        }

        if (this.elementIsPlaceholder(draggable)) {
            return
        }

        this.createPlaceholder(draggable, ev.originalEvent)
        ev.stopPropagation()
        ev.preventDefault()
    }

    ListSortable.prototype.onDragLeave = function(ev) {
        if (!this.isSourceManagedList(ev.originalEvent)) {
            return
        }

        ev.stopPropagation()
        ev.preventDefault()
    }

    ListSortable.prototype.onDragDrop = function(ev) {
        if (!this.isSourceManagedList(ev.originalEvent)) {
            return
        }

        var draggable = this.getClosestDraggableParent(ev.target)
        if (!draggable) {
            return
        }

        this.moveElement(draggable, ev.originalEvent)

        this.removePlaceholders()
    }

    ListSortable.prototype.onDragEnd = function(ev) {
        $(document).off('dragover', this.proxy(this.onDocumentDragOver))
    }

    ListSortable.prototype.onDocumentDragOver = function(ev) {
        if (!this.isSourceManagedList(ev.originalEvent)) {
            return
        }

        if (this.mouseOutsideLists(ev.originalEvent)) {
            this.removePlaceholders()
            return
        }
    }

    ListSortable.prototype.onDocumentMouseMove = function(ev) {
        $(document).off('mousemove', this.proxy(this.onDocumentMouseMove))
        this.removePlaceholders()
    }


    // PLUGIN DEFINITION
    // ============================

    ListSortable.DEFAULTS = {
        handle: null,
        direction: 'vertical'
    }

    var old = $.fn.listSortable

    $.fn.listSortable = function (option) {
        var args = arguments

        return this.each(function () {
            var $this = $(this),
                data  = $this.data('oc.listSortable'),
                options = $.extend({}, ListSortable.DEFAULTS, $this.data(), typeof option == 'object' && option)

            if (!data) {
                $this.data('oc.listSortable', (data = new ListSortable(this, options)))
            }

            if (typeof option == 'string' && data) {
                if (data[option]) {
                    var methodArguments = Array.prototype.slice.call(args) // Clone the arguments array
                    methodArguments.shift()

                    data[option].apply(data, methodArguments)
                }
            }
        })
    }

    $.fn.listSortable.Constructor = ListSortable

    // LISTSORTABLE NO CONFLICT
    // =================

    $.fn.listSortable.noConflict = function () {
        $.fn.listSortable = old
        return this
    }

    $(document).render(function(){
        $('[data-control=list-sortable]').listSortable()
    })

}(window.jQuery);