/**
 * i-doit category folders "drag and drop" tree class.
 */
window.CategoryFoldersDnDTree = Class.create(window.CategoryFoldersTree, {
    options:    {},
    initialize: function ($super, $container, options) {
        $super($container, options);

        this.$container.addClassName('drag-context');
        this.$ghost = $('category-folders-ghost');

        if (!this.$ghost) {
            $('body').insert(this.$ghost = new Element('div', { id: 'category-folders-ghost', className: 'hide' }));
        }

        this.$container.on('click', '.category,.folder-container', function (ev) {
            let $clickedElement = ev.findElement();
            if (!$clickedElement.hasClassName('category') && !$clickedElement.hasClassName('folder-container')) {
                $clickedElement = $clickedElement.up('.category,.folder-container')
            }

            if (!ev.metaKey && !ev.ctrlKey && !ev.shiftKey) {
                this.$container.select('.selected').invoke('removeClassName', 'selected');
            }

            // Find elements between the 'last' and 'current' clicked element.
            if (ev.shiftKey) {
                const $selectables = this.$container.select('.category,.folder-container');
                const fromIndex = $selectables.indexOf(this.$lastClicked);
                const toIndex = $selectables.indexOf($clickedElement);

                const $slice = $selectables.slice(Math.min(fromIndex, toIndex), Math.max(fromIndex, toIndex));

                // Filter out children of 'to-be-selected' folders.
                $slice
                    .filter(($item) => {
                        const $parent = $item.up('.folder-container');
                        return !($parent && $slice.includes($parent));
                    })
                    .invoke('addClassName', 'selected');
            }

            this.$lastClicked = $clickedElement.addClassName('selected');
        }.bind(this));

        return this;
    },

    prepareDraggables: function () {
        const $draggables = this.$container.select('.folder-container,.category');

        // Remove all previous droppables in order to properly reset.
        this.$container.select('.droppable-item:not(.folder-container)').invoke('remove');

        // Create droppables.
        $draggables.forEach(($dragElement) => {
            // Add a droppable before the current drag element.
            $dragElement.insert({before: new Element('div', {className: 'droppable-item'})});

            if ($dragElement.match(':last-child')) {
                // Add a droppable after the last child.
                $dragElement.insert({after: new Element('div', {className: 'droppable-item'})});
            }
        });

        const $droppables = this.$container.select('.droppable-item,.folder-container');

        $droppables.forEach(($drop) => {
            Droppables.add($drop, {
                accept:     'dragged-item',
                hoverclass: 'drop',
                onDrop:     ($draggedElement, $dropElement, event) => {
                    const $draggedItems = this.$container.select('.dragged-item');
                    const isFolder = $dropElement.hasClassName('folder-container');
                    // Check if the old container is now empty.
                    const $oldContainer = $draggedElement.up('.folder-children-container');

                    try {
                        for (let i in $draggedItems) {
                            if (!$draggedItems.hasOwnProperty(i)) {
                                continue;
                            }

                            if (isFolder) {
                                const $folderChildContainer = $dropElement.down('.folder-children-container');

                                    // Check if we dragged into a (previously) empty folder.
                                if (!$folderChildContainer.down('.category,.folder-container') && $folderChildContainer.down('p')) {
                                    $folderChildContainer.down('p').remove();
                                }

                                $folderChildContainer.insert($draggedItems[i]);
                            } else {
                                $dropElement.insert({ before: $draggedItems[i] });
                            }
                        }
                    } catch (e) {
                        // This might happen, when a element is dropped inside itself.
                    }

                    if ($oldContainer && !$oldContainer.down('.category,.folder-container')) {
                        // Check if the container is empty, after the elements have been dragged.
                        $oldContainer.update(this.$emptyFolderHtml);
                    }

                    this.afterDragging();
                }
            });
        });

        $draggables.forEach(($dragElement) => {
            new Draggable($dragElement, {
                revert: true,
                ghosting: true,
                endeffect: Prototype.emptyFunction,
                starteffect: Prototype.emptyFunction,
                snap: (x, y, drag) => [
                    x + (drag.offset[0] - 125), // -125px because the dragged container will be sized to 250px.
                    y + drag.offset[1] + (this.options.$scrollContainer ? this.options.$scrollContainer.scrollTop - 20 : 0)
                ],
                reverteffect: ($element, topOffset, leftOffset) => {
                    new Effect.Morph($element.removeClassName('dragged-item'), {
                        style:       'top: 0; left: 0; width: ' + $element.retrieve('original-width', 600) + 'px; opacity: 1',
                        duration:    0.15,
                        afterFinish: (effect) => {
                            // Remove the specific styles, after the animation finished.
                            effect.element.writeAttribute('style', null);
                        }
                    });
                },
                onStart: this.startDragging.bind(this),
                onDrag: this.duringDragging.bind(this),
                onEnd: this.stopDragging.bind(this)
            });
        });
    },

    startDragging: function (drag, event) {
        const $folderChildren = drag._clone.down('.folder-children');

        this.$droppables = this.$container.select('.droppable-item,.folder-container');

        // Prepare some properties for the 'drag' source element.
        drag.element
            .store('original-width', drag.element.getWidth())
            .setStyle({width: '250px'});

        if ($folderChildren) {
            $folderChildren.select('.droppable-item').invoke('addClassName', 'hide');
        }

        if (!drag._clone.hasClassName('selected')) {
            this.$container.select('.selected').invoke('removeClassName', 'selected');
            drag._clone.addClassName('selected');
        }

        // Remove the 'selected' class from the ghost to not count it.
        drag.element
            .addClassName('dragged-item')
            .removeClassName('selected')
            .setStyle({ opacity: 0 });

        // Prepare the ghosting element.
        const $selectedItems = this.$container.select('.selected').invoke('addClassName', 'dragged-item');
        const ghostContent = $selectedItems.length === 1
            ? $selectedItems[0].down('span').textContent
            : idoit.Translate.get('LC__CATEGORY_FOLDERS__PLACE_N_HERE').replace('{count}', $selectedItems.length);

        this.$ghost.removeClassName('hide').update(ghostContent);

        this.$droppables.invoke('removeClassName', 'open-drop');
    },

    duringDragging: function (drag, event) {
        this.$ghost.setStyle({
            left: (12 + window.mouseX) + 'px',
            top:  window.mouseY + 'px'
        });

        this.initScrolling(window.mouseY, this.options.$scrollContainer.getBoundingClientRect());

        for (let i in this.$droppables) {
            if (!this.$droppables.hasOwnProperty(i)) {
                continue;
            }

            // Check if the element is touching the current mouse position (we include a 10px padding).
            if (!this.elementsTouch(this.$droppables[i], window.mouseX, window.mouseY)) {
                this.$droppables[i].removeClassName('open-drop');
                continue;
            }

            // Simply add the 'open-drop' CSS class. The actual drop will handle the rest.
            this.$droppables[i].addClassName('open-drop');
        }
    },

    stopDragging: function (drag, event) {
        drag.element.addClassName('selected').removeClassName('hide');

        const $selected = this.$container.select('.selected');

        setTimeout(() => $selected.invoke('addClassName', 'selected'), 25);

        this.afterDragging();
    },

    afterDragging: function () {
        // Hide and empty the 'ghost'.
        this.$ghost.addClassName('hide').update('');

        // Hide opened 'drop' areas, unhide directly attached 'drop' elements around the source.
        this.$container.select('.droppable-item,.folder-container')
            .invoke('removeClassName', 'open-drop')
            .invoke('removeClassName', 'hide');

        // Reset the draggable behaviour.
        this.prepareDraggables();

        // Use this to show all elements, if no 'drop' happened.
        setTimeout(() => {
            this.$container.select('.dragged-item,.selected')
                .invoke('removeClassName', 'dragged-item')
                .invoke('writeAttribute', 'style', null);
        }, 175);
    },

    elementsTouch: function ($element, x, y) {
        const offsetTop = Element.cumulativeOffset($element).top;
        const padding = 10;

        // Include scroll offset.
        y += (this.options.$scrollContainer ? this.options.$scrollContainer.scrollTop : 0);

        return y > (offsetTop - padding) && y < (offsetTop + $element.offsetHeight + padding)
    },

    initScrolling: function (mouseY, boundingRect) {
        if ((mouseY - 10) < boundingRect.top && (mouseY + 50) > boundingRect.top) {
            this.stopScrolling();
            this.scrollInterval = setInterval(() => { this.options.$scrollContainer.scrollTop -= 5; }, 10);
        } else if ((mouseY - 50) < (boundingRect.top + boundingRect.height) && (mouseY + 10) > (boundingRect.top + boundingRect.height)) {
            this.stopScrolling();
            this.scrollInterval = setInterval(() => { this.options.$scrollContainer.scrollTop += 5; }, 10);
        } else if (this.scrollInterval) {
            this.stopScrolling();
        }
    },

    stopScrolling: function () {
        if (this.scrollInterval) {
            clearInterval(this.scrollInterval);
        }
    },

    folderNode: function ($super, folder, disableSelection) {
        return $super(folder, disableSelection).insert({ top: new Element('div', { className: 'drag-handle' }) });
    },

    categoryNode: function ($super, category) {
        return $super(category).insert({ top: new Element('div', { className: 'drag-handle' }) });
    }
});
