/**
 * This module is an extension to jQuery ui selectable, which scrolls when you try to select
 * outside the container, and the scrollables are not fitting it originally.
 *
 * Created by László Károlyi <http://linkedin.com/in/karolyi>
 * with the GPLv3 License: http://opensource.org/licenses/GPL-3.0
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
/*global jQuery*/
(function ($) {
  'use strict';
  $.widget('ui.selectableScroll', $.ui.selectable, {
    options: {
      // If an element is passed in here, use it for scrolling instead of widget's element
      scrollElement: null,
      // When the selection is that pixels near to the top/bottom edges, start to scroll
      scrollSnapX: 5,
      // When the selection is that pixels near to the side edges, start to scroll
      scrollSnapY: 5,
      scrollAmount: 25, // In pixels
      scrollIntervalTime: 100 // In milliseconds
    },

    /**
     * This is a slightly modified version of the original _create function,
     * we just add the element's relative positions too
     *
     * @return null
     */
    _create: function () {
      this.element.addClass('ui-selectable');
      this.dragged = false;
      this.helperClasses = ['no-top', 'no-right', 'no-bottom', 'no-left'];
      this.scrollElement = this.options.scrollElement || this.element;
      this.refresh();
      this._mouseInit();
      this.helper = $('<div class="ui-selectable-helper"></div>');
    },

    /**
     * Cache selectee children based on filter
     * @return null
     */
    refresh: function () {
      var elementOffset = this.scrollElement.offset();
      var scrollLeft = this.scrollElement.prop('scrollLeft');
      var scrollTop = this.scrollElement.prop('scrollTop');
      this.selectees = $(this.options.filter, this.scrollElement[0]);
      this.selectees.addClass('ui-selectee');
      this.selectees.each(function () {
        var $element = $(this),
          pos = $element.offset();
        $.data(this, 'selectable-item', {
          element: this,
          $element: $element,
          left: pos.left,
          top: pos.top,
          right: pos.left + $element.outerWidth(),
          bottom: pos.top + $element.outerHeight(),
          // Relative positions according to the element's 0.0
          relative: {
            left: pos.left - elementOffset.left + scrollLeft,
            top: pos.top - elementOffset.top + scrollTop,
            right: pos.left - elementOffset.left + scrollLeft +
              $element.outerWidth(),
            bottom: pos.top - elementOffset.top + scrollTop +
              $element.outerHeight()
          },
          startselected: false,
          selected: $element.hasClass('ui-selected'),
          selecting: $element.hasClass('ui-selecting'),
          unselecting: $element.hasClass('ui-unselecting')
        });
      });
    },

    /**
     * By starting dragging, calculate the element's current scroll states,
     * relative to the elements 0.0 offset
     *
     * @param  object  The mousedown event
     * @return boolean The parent's _mouseStart return value
     */
    _mouseStart: function (event) {
      var pos = this.scrollElement.offset();
      if (
        (event.pageX > this.scrollElement.prop('clientWidth') + pos.left) ||
        (event.pageY > this.scrollElement.prop('clientHeight') + pos.top)
      ) {
        return false;
      }
      var retValue = $.ui.selectable.prototype._mouseStart.call(this,
        event);
      this.lastDragEvent = null;
      this.scrollInfo = {
        // The element's 0.0 offset related to the document element
        elementOffset: this.scrollElement.offset(),
        // The maximum scrollable width (visible width + scrollLeft)
        scrollWidth: this.scrollElement.prop('scrollWidth'),
        // The maximum scrollable height (visible height + scrollTop)
        scrollHeight: this.scrollElement.prop('scrollHeight'),
        // The visible width (minus scrollbars)
        elementWidth: this.scrollElement.prop('clientWidth'),
        // The visible height (minus scrollbars)
        elementHeight: this.scrollElement.prop('clientHeight')
      };
      // Relative to element's 0
      this.scrollInfo.dragStartXPos = event.pageX - this.scrollInfo.elementOffset
        .left + this.scrollElement.prop('scrollLeft');
      // Relative to element's 0
      this.scrollInfo.dragStartYPos = event.pageY - this.scrollInfo.elementOffset
        .top + this.scrollElement.prop('scrollTop');
      this.scrollIntervalId = null;
      return retValue;
    },

    /**
     * Calculate/redraw the helper lasso, keep the helper within
     * the scrolled element
     *
     * @param  object The options object containing the relative positions of the selected rectangle
     * @return null
     */
    _updateHelper: function (options) {
      var x1, y1, x2, y2; // Absolute positions for the lasso helper
      var lassoClassesArray = [];
      if (options.x1 - options.scrollLeft < 0) {
        lassoClassesArray.push('no-left');
        x1 = this.scrollInfo.elementOffset.left;
      } else {
        x1 = this.scrollInfo.elementOffset.left + options.x1 - options.scrollLeft;
      }
      if (options.y1 - options.scrollTop < 0) {
        lassoClassesArray.push('no-top');
        y1 = this.scrollInfo.elementOffset.top;
      } else {
        y1 = this.scrollInfo.elementOffset.top + options.y1 - options.scrollTop;
      }
      if (options.x2 - options.scrollLeft > this.scrollInfo.elementWidth) {
        lassoClassesArray.push('no-right');
        x2 = this.scrollInfo.elementOffset.left + this.scrollInfo.elementWidth;
      } else {
        x2 = this.scrollInfo.elementOffset.left + options.x2 - options.scrollLeft;
      }
      if (options.y2 - options.scrollTop > this.scrollInfo.elementHeight) {
        lassoClassesArray.push('no-bottom');
        y2 = this.scrollInfo.elementOffset.top + this.scrollInfo.elementHeight;
      } else {
        y2 = this.scrollInfo.elementOffset.top + options.y2 - options.scrollTop;
      }
      for (var counter = 0; counter < this.helperClasses.length; counter++) {
        var className = this.helperClasses[counter];
        if ($.inArray(className, lassoClassesArray) !== -1) {
          this.helper.addClass(className);
        } else {
          this.helper.removeClass(className);
        }
      }
      this.helper.css({
        left: x1,
        top: y1,
        width: x2 - x1,
        height: y2 - y1
      });
    },

    /**
     * If the mouse is near to the edges of the elements area, scroll
     *
     * @return object The new scrollLeft and scrollTop values, and a
     * boolean if the element should keep scrolling
     */
    _scrollIfNeeded: function (options) {
      var scrollLeft = this.scrollElement.prop('scrollLeft');
      var scrollTop = this.scrollElement.prop('scrollTop');

      var keepScrolling = false;
      // Scroll if close to edges or over them
      if (this.lastDragEvent.pageX - this.scrollInfo.elementOffset.left <
        this.options.scrollSnapX && scrollLeft > 0) {
        scrollLeft = scrollLeft < this.options.scrollAmount ? 0 :
          scrollLeft - this.options.scrollAmount;
        this.scrollElement.prop('scrollLeft', scrollLeft);
        keepScrolling = true;
      }
      if (this.lastDragEvent.pageY - this.scrollInfo.elementOffset.top <
        this.options.scrollSnapY && scrollTop > 0) {
        scrollTop = scrollTop < this.options.scrollAmount ? 0 : scrollTop -
          this.options.scrollAmount;
        this.scrollElement.prop('scrollTop', scrollTop);
        keepScrolling = true;
      }
      if (this.lastDragEvent.pageX - this.scrollInfo.elementOffset.left >
        this.scrollInfo.elementWidth - this.options.scrollSnapX && this.scrollInfo
        .scrollWidth > scrollLeft + this.scrollInfo.elementWidth) {
        scrollLeft = scrollLeft + this.options.scrollAmount > this.scrollInfo
          .scrollWidth - this.scrollInfo.elementWidth ? this.scrollInfo.scrollWidth -
          this.scrollInfo.elementWidth : scrollLeft + this.options.scrollAmount;
        this.scrollElement.prop('scrollLeft', scrollLeft);
        keepScrolling = true;
      }
      if (this.lastDragEvent.pageY - this.scrollInfo.elementOffset.top >
        this.scrollInfo.elementHeight - this.options.scrollSnapY && this.scrollInfo
        .scrollHeight > scrollLeft + this.scrollInfo.elementHeight) {
        scrollTop = scrollTop + this.options.scrollAmount > this.scrollInfo
          .scrollHeight - this.scrollInfo.elementHeight ? this.scrollInfo
          .scrollHeight - this.scrollInfo.elementHeight : scrollTop +
          this.options.scrollAmount;
        this.scrollElement.prop('scrollTop', scrollTop);
        keepScrolling = true;
      }
      return {
        scrollLeft: scrollLeft,
        scrollTop: scrollTop,
        keepScrolling: keepScrolling
      };
    },

    /**
     * Calculate a relative position to the element's 0.0 offset,
     * from the original drag event's coordinates
     *
     * @param  object The absolute X and Y positions
     * @return object The relative X and Y positions
     */
    _calcRelativePosition: function (options) {
      var relXPos = options.x - this.scrollInfo.elementOffset.left +
        options.scrollLeft;
      var relYPos = options.y - this.scrollInfo.elementOffset.top +
        options.scrollTop;
      return {
        x: relXPos,
        y: relYPos
      };
    },

    /**
     * Calculate relative area positions to offset element
     *
     * @param  object The relative X and Y positions to the 0.0 of the element
     * @return object An object containing the relative area coordinates
     */
    _calcRelativeArea: function (options) {
      var relX1 = options.xPos < this.scrollInfo.dragStartXPos ? options.xPos :
        this.scrollInfo.dragStartXPos;
      var relY1 = options.yPos < this.scrollInfo.dragStartYPos ? options.yPos :
        this.scrollInfo.dragStartYPos;
      var relX2 = options.xPos > this.scrollInfo.dragStartXPos ? options.xPos :
        this.scrollInfo.dragStartXPos;
      var relY2 = options.yPos > this.scrollInfo.dragStartYPos ? options.yPos :
        this.scrollInfo.dragStartYPos;
      return {
        x1: relX1,
        y1: relY1,
        x2: relX2,
        y2: relY2
      };
    },

    /**
     * Update the selected elements
     *
     * @param  object The return value of _calcRelativeArea()
     * @return null
     */
    _updateSelectees: function (options) {
      var that = this;
      this.selectees.each(function () {
        var selectee = $.data(this, 'selectable-item'),
          hit = false;

        // Prevent helper from being selected if appendTo: selectable
        if (!selectee || selectee.element === that.element[0]) {
          return;
        }

        if (that.options.tolerance === 'touch') {
          hit = (!(selectee.relative.left > options.x2 || selectee.relative
            .right < options.x1 || selectee.relative.top > options.y2 ||
            selectee.relative.bottom < options.y1));
        } else if (that.options.tolerance === 'fit') {
          hit = (selectee.relative.left > options.x1 && selectee.relative
            .right < options.x2 && selectee.relative.top > options.y1 &&
            selectee.relative.bottom < options.y2);
        }

        if (hit) {
          // SELECT
          if (selectee.selected) {
            selectee.$element.removeClass('ui-selected');
            selectee.selected = false;
          }
          if (selectee.unselecting) {
            selectee.$element.removeClass('ui-unselecting');
            selectee.unselecting = false;
          }
          if (!selectee.selecting) {
            selectee.$element.addClass('ui-selecting');
            selectee.selecting = true;
            // selectable SELECTING callback
            that._trigger('selecting', that.lastDragEvent, {
              selecting: selectee.element
            });
          }
        } else {
          // UNSELECT
          if (selectee.selecting) {
            if ((that.lastDragEvent.metaKey || that.lastDragEvent.ctrlKey) &&
              selectee.startselected) {
              selectee.$element.removeClass('ui-selecting');
              selectee.selecting = false;
              selectee.$element.addClass('ui-selected');
              selectee.selected = true;
            } else {
              selectee.$element.removeClass('ui-selecting');
              selectee.selecting = false;
              if (selectee.startselected) {
                selectee.$element.addClass('ui-unselecting');
                selectee.unselecting = true;
              }
              // Selectable UNSELECTING callback
              that._trigger('unselecting', that.lastDragEvent, {
                unselecting: selectee.element
              });
            }
          }
          if (selectee.selected) {
            if (!that.lastDragEvent.metaKey && !that.lastDragEvent.ctrlKey && !
              selectee.startselected) {
              selectee.$element.removeClass('ui-selected');
              selectee.selected = false;

              selectee.$element.addClass('ui-unselecting');
              selectee.unselecting = true;
              // Selectable UNSELECTING callback
              that._trigger('unselecting', that.lastDragEvent, {
                unselecting: selectee.element
              });
            }
          }
        }
      });
    },

    /**
     * The original _mouseDrag function overvritten by our one
     *
     * @param  object  The original mousemove event
     * @return boolean Returning false, as the parent returns too
     */
    _mouseDrag: function (event) {
      this.dragged = true;
      if (this.options.disabled) {
        return;
      }
      this.lastDragEvent = event;

      var scrollObj = this._scrollIfNeeded();

      this._updateIntervals({
        keepScrolling: scrollObj.keepScrolling
      });
      this._updateUi({
        doUpdateHelper: true
      });

      return false;
    },

    /**
     * Do the actual calculating/updating of the positions/elements
     *
     * @param  object Options containing if the function should update the helper lasso
     * @return null
     */
    _updateUi: function (options) {
      var scrollObj = this._scrollIfNeeded({
        pageX: this.lastDragEvent.pageX,
        pageY: this.lastDragEvent.pageY
      });
      var relativePos = this._calcRelativePosition({
        x: this.lastDragEvent.pageX,
        y: this.lastDragEvent.pageY,
        scrollLeft: scrollObj.scrollLeft,
        scrollTop: scrollObj.scrollTop
      });
      var relativeArea = this._calcRelativeArea({
        xPos: relativePos.x,
        yPos: relativePos.y
      });
      if (options.doUpdateHelper) {
        this._updateHelper({
          scrollLeft: scrollObj.scrollLeft,
          scrollTop: scrollObj.scrollTop,
          x1: relativeArea.x1,
          y1: relativeArea.y1,
          x2: relativeArea.x2,
          y2: relativeArea.y2
        });
      }
      this._updateSelectees({
        x1: relativeArea.x1,
        y1: relativeArea.y1,
        x2: relativeArea.x2,
        y2: relativeArea.y2
      });
    },

    /**
     * Start the automatic scrolling if needed
     *
     * @param  object Options containing if the function should start the interval
     * @return null
     */
    _updateIntervals: function (options) {
      var that = this;
      if (options.keepScrolling && !this.scrollIntervalId) {
        this.scrollIntervalId = setInterval(function () {
          that._updateUi({
            doUpdateHelper: false
          });
        }, this.options.scrollIntervalTime);
      }
      if (!options.keepScrolling && this.scrollIntervalId)
        this._clearIntervals();
    },

    /**
     * Clear the autoscroll interval
     *
     * @return null
     */
    _clearIntervals: function () {
      // Stop scrolling
      if (this.scrollIntervalId)
        clearInterval(this.scrollIntervalId);
      this.scrollIntervalId = null;
    },

    /**
     * The original _mouseStop event extended with the interval clearer
     *
     * @param  object  The original mousestop event
     * @return boolean The parent's return value
     */
    _mouseStop: function (event) {
      this._clearIntervals();
      var retValue = $.ui.selectable.prototype._mouseStop.call(this,
        event);
      return retValue;
    }
  });
})(jQuery);
