/**
 * TableDnD plug-in for JQuery, allows you to drag and drop table rows
 * You can set up various options to control how the system will work
 * Copyright (c) Denis Howlett <denish@isocra.com>
 * Licensed like jQuery, see http://docs.jquery.com/License.
 *
 * Configuration options:
 *
 * onDragStyle
 *     This is the style that is assigned to the row during drag. There are limitations to the styles that can be
 *     associated with a row (such as you can't assign a border--well you can, but it won't be
 *     displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as
 *     a map (as used in the jQuery css(...) function).
 * onDropStyle
 *     This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations
 *     to what you can do. Also this replaces the original style, so again consider using onDragClass which
 *     is simply added and then removed on drop.
 * onDragClass
 *     This class is added for the duration of the drag and then removed when the row is dropped. It is more
 *     flexible than using onDragStyle since it can be inherited by the row cells and other content. The default
 *     is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your
 *     stylesheet.
 * onDrop
 *     Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table
 *     and the row that was dropped. You can work out the new order of the rows by using
 *     table.rows.
 * onDragStart
 *     Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the
 *     table and the row which the user has started to drag.
 * onAllowDrop
 *     Pass a function that will be called as a row is over another row. If the function returns true, allow
 *     dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under
 *     the cursor. It returns a boolean: true allows the drop, false doesn't allow it.
 * scrollAmount
 *     This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the
 *     window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2,
 *     FF3 beta
 * dragHandle
 *     This is a jQuery mach string for one or more cells in each row that is draggable. If you
 *     specify this, then you are responsible for setting cursor: move in the CSS and only these cells
 *     will have the drag behaviour. If you do not specify a dragHandle, then you get the old behaviour where
 *     the whole row is draggable.
 *
 * Other ways to control behaviour:
 *
 * Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows
 * that you don't want to be draggable.
 *
 * Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form
 * <tableID>[]=<rowID1>&<tableID>[]=<rowID2> so that you can send this back to the server. The table must have
 * an ID as must all the rows.
 *
 * Other methods:
 *
 * $("...").tableDnDUpdate()
 * Will update all the matching tables, that is it will reapply the mousedown method to the rows (or handle cells).
 * This is useful if you have updated the table rows using Ajax and you want to make the table draggable again.
 * The table maintains the original configuration (so you don't have to specify it again).
 *
 * $("...").tableDnDSerialize()
 * Will serialize and return the serialized string as above, but for each of the matching tables--so it can be
 * called from anywhere and isn't dependent on the currentTable being set up correctly before calling
 *
 * Known problems:
 * - Auto-scoll has some problems with IE7  (it scrolls even when it shouldn't), work-around: set scrollAmount to 0
 *
 * Version 0.2: 2008-02-20 First public version
 * Version 0.3: 2008-02-07 Added onDragStart option
 *                         Made the scroll amount configurable (default is 5 as before)
 * Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes
 *                         Added onAllowDrop to control dropping
 *                         Fixed a bug which meant that you couldn't set the scroll amount in both directions
 *                         Added serialize method
 * Version 0.5: 2008-05-16 Changed so that if you specify a dragHandle class it doesn't make the whole row
 *                         draggable
 *                         Improved the serialize method to use a default (and settable) regular expression.
 *                         Added tableDnDupate() and tableDnDSerialize() to be called when you are outside the table
 * Version 0.6: 2011-12-02 Added support for touch devices
 * Version 0.7  2012-04-09 Now works with jQuery 1.7 and supports touch, tidied up tabs and spaces
 */
!(function($, window, document, undefined) {
  // Determine if this is a touch device
  var hasTouch = "ontouchstart" in document.documentElement,
    startEvent = hasTouch ? "touchstart" : "mousedown",
    moveEvent = hasTouch ? "touchmove" : "mousemove",
    endEvent = hasTouch ? "touchend" : "mouseup";

  // If we're on a touch device, then wire up the events
  // see http://stackoverflow.com/a/8456194/1316086
  hasTouch &&
    $.each("touchstart touchmove touchend".split(" "), function(i, name) {
      $.event.fixHooks[name] = $.event.mouseHooks;
    });

  $(document).ready(function() {
    function parseStyle(css) {
      var objMap = {},
        parts = css.match(/([^;:]+)/g) || [];
      while (parts.length) objMap[parts.shift()] = parts.shift().trim();

      return objMap;
    }
    $("table").each(function() {
      if ($(this).data("table") == "dnd") {
        $(this).tableDnD({
          onDragStyle:
            ($(this).data("ondragstyle") &&
              parseStyle($(this).data("ondragstyle"))) ||
            null,
          onDropStyle:
            ($(this).data("ondropstyle") &&
              parseStyle($(this).data("ondropstyle"))) ||
            null,
          onDragClass:
            ($(this).data("ondragclass") == undefined && "tDnD_whileDrag") ||
            $(this).data("ondragclass"),
          onDrop:
            $(this).data("ondrop") &&
            new Function("table", "row", $(this).data("ondrop")), // 'return eval("'+$(this).data('ondrop')+'");') || null,
          onDragStart:
            $(this).data("ondragstart") &&
            new Function("table", "row", $(this).data("ondragstart")), // 'return eval("'+$(this).data('ondragstart')+'");') || null,
          scrollAmount: $(this).data("scrollamount") || 5,
          sensitivity: $(this).data("sensitivity") || 10,
          hierarchyLevel: $(this).data("hierarchylevel") || 0,
          indentArtifact:
            $(this).data("indentartifact") ||
            '<div class="indent">&nbsp;</div>',
          autoWidthAdjust: $(this).data("autowidthadjust") || true,
          autoCleanRelations: $(this).data("autocleanrelations") || true,
          jsonPretifySeparator: $(this).data("jsonpretifyseparator") || "\t",
          serializeRegexp:
            ($(this).data("serializeregexp") &&
              new RegExp($(this).data("serializeregexp"))) ||
            /[^\-]*$/,
          serializeParamName: $(this).data("serializeparamname") || false,
          dragHandle: $(this).data("draghandle") || null
        });
      }
    });
  });

  jQuery.tableDnD = {
    /** Keep hold of the current table being dragged */
    currentTable: null,
    /** Keep hold of the current drag object if any */
    dragObject: null,
    /** The current mouse offset */
    mouseOffset: null,
    /** Remember the old value of X and Y so that we don't do too much processing */
    oldX: 0,
    oldY: 0,

    /** Actually build the structure */
    build: function(options) {
      // Set up the defaults if any

      this.each(function() {
        // This is bound to each matching table, set up the defaults and override with user options
        this.tableDnDConfig = $.extend(
          {
            onDragStyle: null,
            onDropStyle: null,
            // Add in the default class for whileDragging
            onDragClass: "tDnD_whileDrag",
            onDrop: null,
            onDragStart: null,
            scrollAmount: 5,
            /** Sensitivity setting will throttle the trigger rate for movement detection */
            sensitivity: 10,
            /** Hierarchy level to support parent child. 0 switches this functionality off */
            hierarchyLevel: 0,
            /** The html artifact to prepend the first cell with as indentation */
            indentArtifact: '<div class="indent">&nbsp;</div>',
            /** Automatically adjust width of first cell */
            autoWidthAdjust: true,
            /** Automatic clean-up to ensure relationship integrity */
            autoCleanRelations: true,
            /** Specify a number (4) as number of spaces or any indent string for JSON.stringify */
            jsonPretifySeparator: "\t",
            /** The regular expression to use to trim row IDs */
            serializeRegexp: /[^\-]*$/,
            /** If you want to specify another parameter name instead of the table ID */
            serializeParamName: false,
            /** If you give the name of a class here, then only Cells with this class will be draggable */
            dragHandle: null
          },
          options || {}
        );

        // Now make the rows draggable
        $.tableDnD.makeDraggable(this);
        // Prepare hierarchy support
        this.tableDnDConfig.hierarchyLevel && $.tableDnD.makeIndented(this);
      });

      // Don't break the chain
      return this;
    },
    makeIndented: function(table) {
      var config = table.tableDnDConfig,
        rows = table.rows,
        firstCell = $(rows)
          .first()
          .find("td:first")[0],
        indentLevel = 0,
        cellWidth = 0,
        longestCell,
        tableStyle;

      if ($(table).hasClass("indtd")) return null;

      tableStyle = $(table)
        .addClass("indtd")
        .attr("style");
      $(table).css({ whiteSpace: "nowrap" });

      for (var w = 0; w < rows.length; w++) {
        if (
          cellWidth <
          $(rows[w])
            .find("td:first")
            .text().length
        ) {
          cellWidth = $(rows[w])
            .find("td:first")
            .text().length;
          longestCell = w;
        }
      }
      $(firstCell).css({ width: "auto" });
      for (w = 0; w < config.hierarchyLevel; w++)
        $(rows[longestCell])
          .find("td:first")
          .prepend(config.indentArtifact);
      firstCell && $(firstCell).css({ width: firstCell.offsetWidth });
      tableStyle && $(table).css(tableStyle);

      for (w = 0; w < config.hierarchyLevel; w++)
        $(rows[longestCell])
          .find("td:first")
          .children(":first")
          .remove();

      config.hierarchyLevel &&
        $(rows).each(function() {
          indentLevel = $(this).data("level") || 0;
          (indentLevel <= config.hierarchyLevel &&
            $(this).data("level", indentLevel)) ||
            $(this).data("level", 0);
          for (var i = 0; i < $(this).data("level"); i++)
            $(this)
              .find("td:first")
              .prepend(config.indentArtifact);
        });

      return this;
    },
    /** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */
    makeDraggable: function(table) {
      var config = table.tableDnDConfig;

      (config.dragHandle &&
        // We only need to add the event to the specified cells
        $(config.dragHandle, table).each(function() {
          // The cell is bound to "this"
          $(this).bind(startEvent, function(e) {
            $.tableDnD.initialiseDrag(
              $(this).parents("tr")[0],
              table,
              this,
              e,
              config
            );
            return false;
          });
        })) ||
        // For backwards compatibility, we add the event to the whole row
        // get all the rows as a wrapped set
        $(table.rows).each(function() {
          // Iterate through each row, the row is bound to "this"
          if (!$(this).hasClass("nodrag")) {
            $(this).bind(startEvent, function(e) {
              if (e.target.className == "spbsm_dragRow") {
                $.tableDnD.initialiseDrag(this, table, this, e, config);
                return false;
              }
            }); // Store the tableDnD object
          }
        });
    },
    currentOrder: function() {
      var rows = this.currentTable.rows;
      return $.map(rows, function(val) {
        return ($(val).data("level") + val.id).replace(/\s/g, "");
      }).join("");
    },
    initialiseDrag: function(dragObject, table, target, e, config) {
      this.dragObject = dragObject;
      this.currentTable = table;
      this.mouseOffset = this.getMouseOffset(target, e);
      this.originalOrder = this.currentOrder();

      // Now we need to capture the mouse up and mouse move event
      // We can use bind so that we don't interfere with other event handlers
      $(document)
        .bind(moveEvent, this.mousemove)
        .bind(endEvent, this.mouseup);

      // Call the onDragStart method if there is one
      config.onDragStart && config.onDragStart(table, target);
    },
    updateTables: function() {
      this.each(function() {
        // this is now bound to each matching table
        if (this.tableDnDConfig) $.tableDnD.makeDraggable(this);
      });
    },
    /** Get the mouse coordinates from the event (allowing for browser differences) */
    mouseCoords: function(e) {
      if (hasTouch)
        return {
          x: event.changedTouches[0].clientX,
          y: event.changedTouches[0].clientY
        };

      if (e.pageX || e.pageY)
        return {
          x: e.pageX,
          y: e.pageY
        };

      return {
        x: e.clientX + document.body.scrollLeft - document.body.clientLeft,
        y: e.clientY + document.body.scrollTop - document.body.clientTop
      };
    },
    /** Given a target element and a mouse eent, get the mouse offset from that element.
     To do this we need the element's position and the mouse position */
    getMouseOffset: function(target, e) {
      var mousePos, docPos;

      e = e || window.event;

      docPos = this.getPosition(target);
      mousePos = this.mouseCoords(e);

      return {
        x: mousePos.x - docPos.x,
        y: mousePos.y - docPos.y
      };
    },
    /** Get the position of an element by going up the DOM tree and adding up all the offsets */
    getPosition: function(element) {
      var left = 0,
        top = 0;

      // Safari fix -- thanks to Luis Chato for this!
      // Safari 2 doesn't correctly grab the offsetTop of a table row
      // this is detailed here:
      // http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
      // the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
      // note that firefox will return a text node as a first child, so designing a more thorough
      // solution may need to take that into account, for now this seems to work in firefox, safari, ie
      if (element.offsetHeight == 0) element = element.firstChild; // a table cell

      while (element.offsetParent) {
        left += element.offsetLeft;
        top += element.offsetTop;
        element = element.offsetParent;
      }

      left += element.offsetLeft;
      top += element.offsetTop;

      return {
        x: left,
        y: top
      };
    },
    autoScroll: function(mousePos) {
      var config = this.currentTable.tableDnDConfig,
        yOffset = window.pageYOffset,
        windowHeight = window.innerHeight
          ? window.innerHeight
          : document.documentElement.clientHeight
          ? document.documentElement.clientHeight
          : document.body.clientHeight;

      // Windows version
      // yOffset=document.body.scrollTop;
      if (document.all)
        if (
          typeof document.compatMode != "undefined" &&
          document.compatMode != "BackCompat"
        )
          yOffset = document.documentElement.scrollTop;
        else if (typeof document.body != "undefined")
          yOffset = document.body.scrollTop;

      (mousePos.y - yOffset < config.scrollAmount &&
        window.scrollBy(0, -config.scrollAmount)) ||
        (windowHeight - (mousePos.y - yOffset) < config.scrollAmount &&
          window.scrollBy(0, config.scrollAmount));
    },
    moveVerticle: function(moving, currentRow) {
      if (
        0 != moving.vertical &&
        // If we're over a row then move the dragged row to there so that the user sees the
        // effect dynamically
        currentRow &&
        this.dragObject != currentRow &&
        this.dragObject.parentNode == currentRow.parentNode
      )
        (0 > moving.vertical &&
          this.dragObject.parentNode.insertBefore(
            this.dragObject,
            currentRow.nextSibling
          )) ||
          (0 < moving.vertical &&
            this.dragObject.parentNode.insertBefore(
              this.dragObject,
              currentRow
            ));
    },
    moveHorizontal: function(moving, currentRow) {
      var config = this.currentTable.tableDnDConfig,
        currentLevel;

      if (
        !config.hierarchyLevel ||
        0 == moving.horizontal ||
        // We only care if moving left or right on the current row
        !currentRow ||
        this.dragObject != currentRow
      )
        return null;

      currentLevel = $(currentRow).data("level");

      0 < moving.horizontal &&
        currentLevel > 0 &&
        $(currentRow)
          .find("td:first")
          .children(":first")
          .remove() &&
        $(currentRow).data("level", --currentLevel);

      0 > moving.horizontal &&
        currentLevel < config.hierarchyLevel &&
        $(currentRow)
          .prev()
          .data("level") >= currentLevel &&
        $(currentRow)
          .children(":first")
          .prepend(config.indentArtifact) &&
        $(currentRow).data("level", ++currentLevel);
    },
    mousemove: function(e) {
      var dragObj = $($.tableDnD.dragObject),
        config = $.tableDnD.currentTable.tableDnDConfig,
        currentRow,
        mousePos,
        moving,
        x,
        y;

      e && e.preventDefault();

      if (!$.tableDnD.dragObject) return false;

      // prevent touch device screen scrolling
      e.type == "touchmove" && event.preventDefault(); // TODO verify this is event and not really e

      // update the style to show we're dragging
      (config.onDragClass && dragObj.addClass(config.onDragClass)) ||
        dragObj.css(config.onDragStyle);

      mousePos = $.tableDnD.mouseCoords(e);
      x = mousePos.x - $.tableDnD.mouseOffset.x;
      y = mousePos.y - $.tableDnD.mouseOffset.y;

      // auto scroll the window
      $.tableDnD.autoScroll(mousePos);

      currentRow = $.tableDnD.findDropTargetRow(dragObj, y);
      moving = $.tableDnD.findDragDirection(x, y);

      $.tableDnD.moveVerticle(moving, currentRow);
      $.tableDnD.moveHorizontal(moving, currentRow);

      return false;
    },
    findDragDirection: function(x, y) {
      var sensitivity = this.currentTable.tableDnDConfig.sensitivity,
        oldX = this.oldX,
        oldY = this.oldY,
        xMin = oldX - sensitivity,
        xMax = oldX + sensitivity,
        yMin = oldY - sensitivity,
        yMax = oldY + sensitivity,
        moving = {
          horizontal: x >= xMin && x <= xMax ? 0 : x > oldX ? -1 : 1,
          vertical: y >= yMin && y <= yMax ? 0 : y > oldY ? -1 : 1
        };

      // update the old value
      if (moving.horizontal != 0) this.oldX = x;
      if (moving.vertical != 0) this.oldY = y;

      return moving;
    },
    /** We're only worried about the y position really, because we can only move rows up and down */
    findDropTargetRow: function(draggedRow, y) {
      var rowHeight = 0,
        rows = this.currentTable.rows,
        config = this.currentTable.tableDnDConfig,
        rowY = 0,
        row = null;

      for (var i = 0; i < rows.length; i++) {
        row = rows[i];
        rowY = this.getPosition(row).y;
        rowHeight = parseInt(row.offsetHeight) / 2;
        if (row.offsetHeight == 0) {
          rowY = this.getPosition(row.firstChild).y;
          rowHeight = parseInt(row.firstChild.offsetHeight) / 2;
        }
        // Because we always have to insert before, we need to offset the height a bit
        if (y > rowY - rowHeight && y < rowY + rowHeight)
          if (
            draggedRow.is(row) ||
            (config.onAllowDrop && !config.onAllowDrop(draggedRow, row)) ||
            // If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic)
            $(row).hasClass("nodrop")
          )
            // that's the row we're over
            // If it's the same as the current row, ignore it
            return null;
          else return row;
      }
      return null;
    },
    processMouseup: function() {
      var config = this.currentTable.tableDnDConfig,
        droppedRow = this.dragObject,
        parentLevel = 0,
        myLevel = 0;

      if (!this.currentTable || !droppedRow) return null;

      // Unbind the event handlers
      $(document)
        .unbind(moveEvent, this.mousemove)
        .unbind(endEvent, this.mouseup);

      config.hierarchyLevel &&
        config.autoCleanRelations &&
        $(this.currentTable.rows)
          .first()
          .find("td:first")
          .children()
          .each(function() {
            myLevel = $(this)
              .parents("tr:first")
              .data("level");
            myLevel &&
              $(this)
                .parents("tr:first")
                .data("level", --myLevel) &&
              $(this).remove();
          }) &&
        config.hierarchyLevel > 1 &&
        $(this.currentTable.rows).each(function() {
          myLevel = $(this).data("level");
          if (myLevel > 1) {
            parentLevel = $(this)
              .prev()
              .data("level");
            while (myLevel > parentLevel + 1) {
              $(this)
                .find("td:first")
                .children(":first")
                .remove();
              $(this).data("level", --myLevel);
            }
          }
        });

      // If we have a dragObject, then we need to release it,
      // The row will already have been moved to the right place so we just reset stuff
      (config.onDragClass && $(droppedRow).removeClass(config.onDragClass)) ||
        $(droppedRow).css(config.onDropStyle);

      this.dragObject = null;
      // Call the onDrop method if there is one
      config.onDrop &&
        this.originalOrder != this.currentOrder() &&
        $(droppedRow)
          .hide()
          .fadeIn("fast") &&
        config.onDrop(this.currentTable, droppedRow);

      this.currentTable = null; // let go of the table too
    },
    mouseup: function(e) {
      e && e.preventDefault();
      $.tableDnD.processMouseup();
      spbsmNM.func.updateRows();
      return false;
    },
    jsonize: function(pretify) {
      var table = this.currentTable;
      if (pretify)
        return JSON.stringify(
          this.tableData(table),
          null,
          table.tableDnDConfig.jsonPretifySeparator
        );
      return JSON.stringify(this.tableData(table));
    },
    serialize: function() {
      return $.param(this.tableData(this.currentTable));
    },
    serializeTable: function(table) {
      var result = "";
      var paramName = table.tableDnDConfig.serializeParamName || table.id;
      var rows = table.rows;
      for (var i = 0; i < rows.length; i++) {
        if (result.length > 0) result += "&";
        var rowId = rows[i].id;
        if (
          rowId &&
          table.tableDnDConfig &&
          table.tableDnDConfig.serializeRegexp
        ) {
          rowId = rowId.match(table.tableDnDConfig.serializeRegexp)[0];
          result += paramName + "[]=" + rowId;
        }
      }
      return result;
    },
    serializeTables: function() {
      var result = [];
      $("table").each(function() {
        this.id && result.push($.param(this.tableData(this)));
      });
      return result.join("&");
    },
    tableData: function(table) {
      var config = table.tableDnDConfig,
        previousIDs = [],
        currentLevel = 0,
        indentLevel = 0,
        rowID = null,
        data = {},
        getSerializeRegexp,
        paramName,
        currentID,
        rows;

      if (!table) table = this.currentTable;
      if (!table || !table.id || !table.rows || !table.rows.length)
        return {
          error: {
            code: 500,
            message: "Not a valid table, no serializable unique id provided."
          }
        };

      rows =
        (config.autoCleanRelations && table.rows) || $.makeArray(table.rows);
      paramName = config.serializeParamName || table.id;
      currentID = paramName;

      getSerializeRegexp = function(rowId) {
        if (rowId && config && config.serializeRegexp)
          return rowId.match(config.serializeRegexp)[0];
        return rowId;
      };

      data[currentID] = [];
      !config.autoCleanRelations &&
        $(rows[0]).data("level") &&
        rows.unshift({ id: "undefined" });

      for (var i = 0; i < rows.length; i++) {
        if (config.hierarchyLevel) {
          indentLevel = $(rows[i]).data("level") || 0;
          if (indentLevel == 0) {
            currentID = paramName;
            previousIDs = [];
          } else if (indentLevel > currentLevel) {
            previousIDs.push([currentID, currentLevel]);
            currentID = getSerializeRegexp(rows[i - 1].id);
          } else if (indentLevel < currentLevel) {
            for (var h = 0; h < previousIDs.length; h++) {
              if (previousIDs[h][1] == indentLevel)
                currentID = previousIDs[h][0];
              if (previousIDs[h][1] >= currentLevel) previousIDs[h][1] = 0;
            }
          }
          currentLevel = indentLevel;

          if (!$.isArray(data[currentID])) data[currentID] = [];
          rowID = getSerializeRegexp(rows[i].id);
          rowID && data[currentID].push(rowID);
        } else {
          rowID = getSerializeRegexp(rows[i].id);
          rowID && data[currentID].push(rowID);
        }
      }
      return data;
    }
  };

  jQuery.fn.extend({
    tableDnD: $.tableDnD.build,
    tableDnDUpdate: $.tableDnD.updateTables,
    tableDnDSerialize: $.proxy($.tableDnD.serialize, $.tableDnD),
    tableDnDSerializeAll: $.tableDnD.serializeTables,
    tableDnDData: $.proxy($.tableDnD.tableData, $.tableDnD)
  });
})(jQuery, window, window.document);
