/**
 * @file
 * Extends methods from core/misc/tabledrag.js.
 */
(function ($) {

  // Save the original prototype.
  var prototype = Drupal.tableDrag.prototype;

  /**
   * Provides table and field manipulation.
   *
   * @constructor
   *
   * @param {HTMLElement} table
   *   DOM object for the table to be made draggable.
   * @param {object} tableSettings
   *   Settings for the table added via drupal_add_dragtable().
   */
  Drupal.tableDrag = function (table, tableSettings) {
    var self = this;
    var $table = $(table);

    /**
     * @type {jQuery}
     */
    this.$table = $(table);

    /**
     *
     * @type {HTMLElement}
     */
    this.table = table;

    /**
     * @type {object}
     */
    this.tableSettings = tableSettings;

    /**
     * Used to hold information about a current drag operation.
     *
     * @type {?HTMLElement}
     */
    this.dragObject = null;

    /**
     * Provides operations for row manipulation.
     *
     * @type {?HTMLElement}
     */
    this.rowObject = null;

    /**
     * Remember the previous element.
     *
     * @type {?HTMLElement}
     */
    this.oldRowElement = null;

    /**
     * Used to determine up or down direction from last mouse move.
     *
     * @type {number}
     */
    this.oldY = 0;

    /**
     * Whether anything in the entire table has changed.
     *
     * @type {bool}
     */
    this.changed = false;

    /**
     * Maximum amount of allowed parenting.
     *
     * @type {number}
     */
    this.maxDepth = 0;

    /**
     * Direction of the table.
     *
     * @type {number}
     */
    this.rtl = $(this.table).css('direction') === 'rtl' ? -1 : 1;

    /**
     *
     * @type {bool}
     */
    this.striping = $(this.table).data('striping') === 1;

    /**
     * Configure the scroll settings.
     *
     * @type {object}
     *
     * @prop {number} amount
     * @prop {number} interval
     * @prop {number} trigger
     */
    this.scrollSettings = {amount: 4, interval: 50, trigger: 70};

    /**
     *
     * @type {?number}
     */
    this.scrollInterval = null;

    /**
     *
     * @type {number}
     */
    this.scrollY = 0;

    /**
     *
     * @type {number}
     */
    this.windowHeight = 0;

    /**
     * @type {?HTMLElement}
     */
    this.$toggleWeightButton = null;

    /**
     * Check this table's settings to see if there are parent relationships in
     * this table. For efficiency, large sections of code can be skipped if we
     * don't need to track horizontal movement and indentations.
     *
     * @type {bool}
     */
    this.indentEnabled = false;
    for (var group in tableSettings) {
      if (tableSettings.hasOwnProperty(group)) {
        for (var n in tableSettings[group]) {
          if (tableSettings[group].hasOwnProperty(n)) {
            if (tableSettings[group][n].relationship === 'parent') {
              this.indentEnabled = true;
            }
            if (tableSettings[group][n].limit > 0) {
              this.maxDepth = tableSettings[group][n].limit;
            }
          }
        }
      }
    }
    if (this.indentEnabled) {

      /**
       * Total width of indents, set in makeDraggable.
       *
       * @type {number}
       */
      this.indentCount = 1;
      // Find the width of indentations to measure mouse movements against.
      // Because the table doesn't need to start with any indentations, we
      // manually append 2 indentations in the first draggable row, measure
      // the offset, then remove.
      var indent = Drupal.theme('tableDragIndentation');
      var testRow = $('<tr/>').addClass('draggable').appendTo(table);
      var testCell = $('<td/>').appendTo(testRow).prepend(indent).prepend(indent);
      var $indentation = testCell.find('.js-indentation');

      /**
       *
       * @type {number}
       */
      this.indentAmount = $indentation.get(1).offsetLeft - $indentation.get(0).offsetLeft;
      testRow.remove();
    }

    // Make each applicable row draggable.
    // Match immediate children of the parent element to allow nesting.
    $table.find('> tr.draggable, > tbody > tr.draggable').each(function () { self.makeDraggable(this); });

    // Add a link before the table for users to show or hide weight columns.
    self.$toggleWeightButton = $(Drupal.theme('btn-sm', {
      'class': ['tabledrag-toggle-weight'],
      'data-drupal-selector': ['tabledrag-toggle-weight'],
      title: Drupal.t('Re-order rows by numerical weight instead of dragging.'),
      'data-toggle': 'tooltip'
    }));

    self.$toggleWeightButton = $('[data-drupal-selector="tabledrag-toggle-weight"]');

    self.$toggleWeightButton
      .on('click', $.proxy(function (e) {
        e.preventDefault();
        this.toggleColumns();
      }, this))
      .wrap('<div class="tabledrag-toggle-weight-wrapper"></div>')
      .parent();

    $table.before(self.$toggleWeightButton);

    // Initialize the specified columns (for example, weight or parent columns)
    // to show or hide according to user preference. This aids accessibility
    // so that, e.g., screen reader users can choose to enter weight values and
    // manipulate form elements directly, rather than using drag-and-drop..
    self.initColumns();

    // Add event bindings to the document. The self variable is passed along
    // as event handlers do not have direct access to the tableDrag object.
    $(document).on('touchmove', function (event) { return self.dragRow(event.originalEvent.touches[0], self); });
    $(document).on('touchend', function (event) { return self.dropRow(event.originalEvent.touches[0], self); });
    $(document).on('mousemove pointermove', function (event) { return self.dragRow(event, self); });
    $(document).on('mouseup pointerup', function (event) { return self.dropRow(event, self); });

    // React to localStorage event showing or hiding weight columns.
    $(window).on('storage', $.proxy(function (e) {
      // Only react to 'Drupal.tableDrag.showWeight' value change.
      if (e.originalEvent.key === 'Drupal.tableDrag.showWeight') {
        // This was changed in another window, get the new value for this
        // window.
        showWeight = JSON.parse(e.originalEvent.newValue);
        this.displayColumns(showWeight);
      }
    }, this));
  };

  // Restore the original prototype.
  Drupal.tableDrag.prototype = prototype;

  /**
   * Take an item and add event handlers to make it become draggable.
   *
   * @param {HTMLElement} item
   */
  Drupal.tableDrag.prototype.makeDraggable = function (item) {
    var self = this;
    var $item = $(item);

    // Add a class to the title link
    $item.find('td:first-of-type').find('a').addClass('menu-item__link');

    // Create the handle.
    var handle = $('<a href="#" class="tabledrag-handle"/>');

    // Insert the handle after indentations (if any).
    var $indentationLast = $item.find('td:first-of-type').find('.js-indentation').eq(-1);
    if ($indentationLast.length) {
      $indentationLast.after(handle);
      // Update the total width of indentation in this entire table.
      self.indentCount = Math.max($item.find('.js-indentation').length, self.indentCount);
    }
    else {
      $item.find('td').eq(0).prepend(handle);
    }

    // Add the glyphicon to the handle.
    handle
      .attr('title', Drupal.t('Drag to re-order'))
      .attr('data-toggle', 'tooltip')
      .append(Drupal.theme('bootstrapIcon', 'move'))
    ;

    handle.on('mousedown touchstart pointerdown', function (event) {
      event.preventDefault();
      if (event.originalEvent.type === 'touchstart') {
        event = event.originalEvent.touches[0];
      }
      self.dragStart(event, self, item);
    });

    // Prevent the anchor tag from jumping us to the top of the page.
    handle.on('click', function (e) {
      e.preventDefault();
    });

    // Set blur cleanup when a handle is focused.
    handle.on('focus', function () {
      self.safeBlur = true;
    });

    // On blur, fire the same function as a touchend/mouseup. This is used to
    // update values after a row has been moved through the keyboard support.
    handle.on('blur', function (event) {
      if (self.rowObject && self.safeBlur) {
        self.dropRow(event, self);
      }
    });

    // Add arrow-key support to the handle.
    handle.on('keydown', function (event) {
      // If a rowObject doesn't yet exist and this isn't the tab key.
      if (event.keyCode !== 9 && !self.rowObject) {
        self.rowObject = new self.row(item, 'keyboard', self.indentEnabled, self.maxDepth, true);
      }

      var keyChange = false;
      var groupHeight;
      switch (event.keyCode) {
        // Left arrow.
        case 37:
        // Safari left arrow.
        case 63234:
          keyChange = true;
          self.rowObject.indent(-1 * self.rtl);
          break;

        // Up arrow.
        case 38:
        // Safari up arrow.
        case 63232:
          var $previousRow = $(self.rowObject.element).prev('tr:first-of-type');
          var previousRow = $previousRow.get(0);
          while (previousRow && $previousRow.is(':hidden')) {
            $previousRow = $(previousRow).prev('tr:first-of-type');
            previousRow = $previousRow.get(0);
          }
          if (previousRow) {
            // Do not allow the onBlur cleanup.
            self.safeBlur = false;
            self.rowObject.direction = 'up';
            keyChange = true;

            if ($(item).is('.tabledrag-root')) {
              // Swap with the previous top-level row.
              groupHeight = 0;
              while (previousRow && $previousRow.find('.js-indentation').length) {
                $previousRow = $(previousRow).prev('tr:first-of-type');
                previousRow = $previousRow.get(0);
                groupHeight += $previousRow.is(':hidden') ? 0 : previousRow.offsetHeight;
              }
              if (previousRow) {
                self.rowObject.swap('before', previousRow);
                // No need to check for indentation, 0 is the only valid one.
                window.scrollBy(0, -groupHeight);
              }
            }
            else if (self.table.tBodies[0].rows[0] !== previousRow || $previousRow.is('.draggable')) {
              // Swap with the previous row (unless previous row is the first
              // one and undraggable).
              self.rowObject.swap('before', previousRow);
              self.rowObject.interval = null;
              self.rowObject.indent(0);
              window.scrollBy(0, -parseInt(item.offsetHeight, 10));
            }
            // Regain focus after the DOM manipulation.
            handle.trigger('focus');
          }
          break;

        // Right arrow.
        case 39:
        // Safari right arrow.
        case 63235:
          keyChange = true;
          self.rowObject.indent(self.rtl);
          break;

        // Down arrow.
        case 40:
        // Safari down arrow.
        case 63233:
          var $nextRow = $(self.rowObject.group).eq(-1).next('tr:first-of-type');
          var nextRow = $nextRow.get(0);
          while (nextRow && $nextRow.is(':hidden')) {
            $nextRow = $(nextRow).next('tr:first-of-type');
            nextRow = $nextRow.get(0);
          }
          if (nextRow) {
            // Do not allow the onBlur cleanup.
            self.safeBlur = false;
            self.rowObject.direction = 'down';
            keyChange = true;

            if ($(item).is('.tabledrag-root')) {
              // Swap with the next group (necessarily a top-level one).
              groupHeight = 0;
              var nextGroup = new self.row(nextRow, 'keyboard', self.indentEnabled, self.maxDepth, false);
              if (nextGroup) {
                $(nextGroup.group).each(function () {
                  groupHeight += $(this).is(':hidden') ? 0 : this.offsetHeight;
                });
                var nextGroupRow = $(nextGroup.group).eq(-1).get(0);
                self.rowObject.swap('after', nextGroupRow);
                // No need to check for indentation, 0 is the only valid one.
                window.scrollBy(0, parseInt(groupHeight, 10));
              }
            }
            else {
              // Swap with the next row.
              self.rowObject.swap('after', nextRow);
              self.rowObject.interval = null;
              self.rowObject.indent(0);
              window.scrollBy(0, parseInt(item.offsetHeight, 10));
            }
            // Regain focus after the DOM manipulation.
            handle.trigger('focus');
          }
          break;
      }

      if (self.rowObject && self.rowObject.changed === true) {
        $(item).addClass('drag');
        if (self.oldRowElement) {
          $(self.oldRowElement).removeClass('drag-previous');
        }
        self.oldRowElement = item;
        if (self.striping === true) {
          self.restripeTable();
        }
        self.onDrag();
      }

      // Returning false if we have an arrow key to prevent scrolling.
      if (keyChange) {
        return false;
      }
    });

    // Compatibility addition, return false on keypress to prevent unwanted
    // scrolling. IE and Safari will suppress scrolling on keydown, but all
    // other browsers need to return false on keypress.
    // http://www.quirksmode.org/js/keys.html
    handle.on('keypress', function (event) {
      switch (event.keyCode) {
        // Left arrow.
        case 37:
        // Up arrow.
        case 38:
        // Right arrow.
        case 39:
        // Down arrow.
        case 40:
          return false;
      }
    });
  };

  /**
   * Add an asterisk or other marker to the changed row.
   */
  Drupal.tableDrag.prototype.row.prototype.markChanged = function () {
    var $cell = $('td:first', this.element);
    // Find the first appropriate place to insert the marker.
    var $target = $($cell.find('.file-size').get(0) || $cell.find('.file').get(0) || $cell.find('.tabledrag-handle').get(0));
    if (!$cell.find('.tabledrag-changed').length) {
      $target.after(' ' + Drupal.theme('tableDragChangedMarker') + ' ');
    }
  };

  $.extend(Drupal.theme, /** @lends Drupal.theme */{

    /**
     * @return {string}
     */
    tableDragChangedMarker: function () {
      return Drupal.theme('bootstrapIcon', 'warning-sign', {'class': ['tabledrag-changed', 'text-warning']});
    },

    /**
     * @return {string}
     */
    tableDragChangedWarning: function () {
      return '<div class="tabledrag-changed-warning alert alert-sm alert-warning messages warning">' + Drupal.theme('tableDragChangedMarker') + ' ' + Drupal.t('You have unsaved changes.') + '</div>';
    }
  });

})(jQuery);
