/**
 * @file
 * Paragraphs drag and drop handling and integration with the Sortable library.
 */

(function ($, Drupal) {

  'use strict';

  /**
   * jQuery plugin for Sortable
   *
   * Registers Sortable under a custom name to prevent a collision with jQuery
   * UI.
   *
   * @param   {Object|String} options
   * @param   {..*}           [args]
   * @returns {jQuery|*}
   */
  $.fn.paragraphsSortable = function (options) {
    var retVal,
      args = arguments;

    this.each(function () {
      var $el = $(this),
        sortable = $el.data('sortable');

      if (!sortable && (options instanceof Object || !options)) {
        sortable = new Sortable(this, options);
        $el.data('sortable', sortable);
      }

      if (sortable) {
        if (options === 'widget') {
          return sortable;
        }
        else if (options === 'destroy') {
          sortable.destroy();
          $el.removeData('sortable');
        }
        else if (typeof sortable[options] === 'function') {
          retVal = sortable[options].apply(sortable, [].slice.call(args, 1));
        }
        else if (options in sortable.options) {
          retVal = sortable.option.apply(sortable, args);
        }
      }
    });

    return (retVal === void 0) ? this : retVal;
  };


  Drupal.behaviors.paragraphsDraggable = {
    attach: function (context) {
      // Prevent default click handling on the drag handle.
      $('.paragraphs-dragdrop__handle', context).on('click', function (event) {
        event.preventDefault();
      });

      // Initialize drag and drop.
      $('ul.paragraphs-dragdrop__list', context).each(function (i, item) {
        $(item).paragraphsSortable({
          group: "paragraphs",
          sort: true,
          handle: ".paragraphs-dragdrop__handle",
          onMove: isAllowed,
          onEnd: function(evt) {
            handleReorder(evt);
            endDragClasses();
          },
          onStart: startDragClasses,
        });
      });

      /**
       * Callback to update weight and path information.
       *
       * @param evt
       *   The Sortable event.
       */
      function handleReorder(evt) {
        // Update both the source and target children.
        if (evt.from === evt.to) {
          updateWeightsAndPath($(evt.to).children('li'));
        }
        else {
          updateWeightsAndPath($(evt.from).children('li'));
          updateWeightsAndPath($(evt.to).children('li'));
        }
        endDragClasses();
      }

      /**
       * Update weight and recursively update path of the provided paragraphs.
       *
       * @param $items
       *   Drag and drop items.
       */
      function updateWeightsAndPath($items) {
        $items.each(function (index, value) {

          // Update the weight in the weight of the current element, avoid
          // matching child weights by selecting the first.
          var $currentItem = $(value);
          var $weight = $currentItem.find('.paragraphs-dragdrop__weight:first');
          $weight.val(index);

          // Update the path of the current element and then update all nested
          // elements.
          updatePaths($currentItem, $currentItem.parent());
          $currentItem.find('> div > ul').each(function () {
            updateNestedPath(this, index, $currentItem);
          });
        })
      }

      /**
       * Update the path field based on the parent.
       *
       * @param $item
       *   A list item.
       * @param $parent
       *   The parent of the list item.
       */
      function updatePaths($item, $parent) {
        // Select the first path field which is the one from the current
        // element.
        var $pathField = $item.find('.paragraphs-dragdrop__path:first');
        var newPath = $parent.attr('data-paragraphs-dragdrop-path');
        $pathField.val(newPath);
      }

      /**
       * Update nested paragraphs for a field/list.
       *
       * @param childList
       *   The paragraph field/list, parent of the children to be updated.
       * @param parentIndex
       *   The index of the parent list item.
       * @param $parentListItem
       *   The parent list item.
       */
      function updateNestedPath(childList, parentIndex, $parentListItem) {

        var sortablePath = childList.getAttribute('data-paragraphs-dragdrop-path');
        var newParent = $parentListItem.parent().attr('data-paragraphs-dragdrop-path');

        // Update the data attribute of the list based on the parent index and
        // list item.
        sortablePath = newParent + "][" + parentIndex + sortablePath.substr(sortablePath.lastIndexOf("]"));
        childList.setAttribute('data-paragraphs-dragdrop-path', sortablePath);

        // Now update the children.
        $(childList).children().each(function (childIndex) {
          var $childListItem = $(this);
          updatePaths($childListItem, $(childList), childIndex);
          $(this).find('> div > ul').each(function () {
            var nestedChildList = this;
            updateNestedPath(nestedChildList, childIndex, $childListItem);
          });
        });
      }


      /**
       * Callback to check if a paragraph item can be dropped into a position.
       *
       * @param evt
       *   The Sortable event.
       * @param originalEvent
       *   The original Sortable event.
       *
       * @returns {boolean|*}
       *   True if the type is allowed and there is enough room.
       */
      function isAllowed(evt, originalEvent) {
        var dragee = evt.dragged;
        var target = evt.to;
        var drageeType = dragee.dataset.paragraphsDragdropBundle;
        var allowedTypes = target.dataset.paragraphsDragdropAllowedTypes;
        var hasSameContainer = evt.to === evt.from;
        var allowed = hasSameContainer || (contains(drageeType, allowedTypes) && hasRoom(target));
        targetAllowedClasses(target, allowed);

        return allowed;
      }

      /**
       * Checks if the target has room.
       *
       * @param target
       *   The target list/paragraph field.
       *
       * @returns {boolean}
       *   True if the field is unlimited or limit is not reached yet.
       */
      function hasRoom(target) {

        var cardinality = target.dataset.paragraphsDragdropCardinality;
        var occupants = target.childNodes.length;
        var isLimited = parseInt(cardinality, 10) !== -1;
        var hasRoom = cardinality > occupants;

        return hasRoom || !isLimited;
      }

      /**
       * Checks if the paragraph type is allowed in the target type list.
       *
       * @param candidate
       *   The paragraph type.
       * @param set
       *   Comma separated list of target types.
       *
       * @returns {boolean}
       *   TRUE if the target type is allowed.
       */
      function contains(candidate, set) {
        set = set.split(',');
        var l = set.length;

        for(var i = 0; i < l; i++) {
          if(set[i] === candidate) {
            return true;
          }
        }
        return false;
      }

      /**
       * Provides a helper class indicating drag status on <html> element when
       * dragging starts.
       */
      function startDragClasses() {
        $('html').addClass('is-dragging-paragraphs');
        // Fix race condition when the drag event start results in a scrollbar
        // position change triggered by a collapsing item with children. Add a
        // min-height for the current height as a workaround.
        $('.paragraphs-dragdrop__list').eq(0).css('min-height', function() {
          return $(this).height();
        });
      }

      /**
       * Provides a helper class indicating a valid drop target via isAllowed().
       *
       * @param target
       *   The target list/paragraph field.
       *
       * @param {boolean} allowed
       *   TRUE if the target type is allowed.
       */
      function targetAllowedClasses(target, allowed) {
        $('.is-droppable-target').removeClass('is-droppable-target');
        if (allowed) {
          $(target).addClass('is-droppable-target');
        }
      }

      /**
       * Removes helper classes when dragging ends.
       */
      function endDragClasses() {
        $('html').removeClass('is-dragging-paragraphs');
        $('.is-droppable-target').removeClass('is-droppable-target');
        // Remove the custom min-height definition added in startDragClasses().
        $('.paragraphs-dragdrop__list').eq(0).removeAttr('style');
      }

      // Fix for an iOS 10 bug. Binding empty event handler on the touchmove
      // event.
      window.addEventListener('touchmove', function () {
      })
    }
  }

})(jQuery, Drupal);
