//= require comments

//************************************
// CONSTANTS
//************************************

var DRAG_INVALID = 0;
var DRAG_MOUSE = 1;
var DRAG_TOUCH = 2;

// This JS code also contains some .css styling instructions.
// Those defaults are used in code where there is a "CSS_STYLE" comment.
var DEFAULT_ENDPOINT_STYLE = "Blank";
var DEFAULT_CONNECTION_HOVER_STYLE =
{
  lineWidth: 5
};
var DEFAULT_CONNECTION_OVERLAY_STYLE =
[ "Arrow", {
  location: 1,
  id: "arrow",
  length: 12,
  width: 10,
  foldback: 1
} ];
var DEFAULT_CONNECTION_LABEL_STYLE =
{
  label: "x",
  id: "label",
  cssClass: "connLabel"
};
var DEFAULT_ANCHOR_STYLE = "Continuous";
var DEFAULT_CONNECTOR_STYLE =
[ "Straight", {
  gap: 2
} ];
var DEFAULT_CONNECTOR_STYLE_2 =
{
  strokeStyle: '<%= Constants::COLOR_WHITE %>',
  lineWidth: 1.5,
  outlineColor: "transparent",
  outlineWidth: 0
};

// Zoom-level specific variables
//var GRID_DIST_EDIT_X = 300;
//var GRID_DIST_EDIT_Y = 140; //135
var GRID_DIST_EDIT_X = 10;
var GRID_DIST_EDIT_Y = 10;
var GRID_DIST_EDIT_STEP_X = 10;
var GRID_DIST_EDIT_STEP_Y = 10;
var EDIT_ENDPOINT_STYLE =
[ "Dot", {
  radius: 4,
  cssClass: "ep-normal",
  hoverClass: "ep-hover"
} ];
var EDIT_CONNECTOR_STYLE_2 =
{
  strokeStyle: '<%= Constants::COLOR_WHITE %>',
  lineWidth: 3,
  outlineColor: "transparent",
  outlineWidth: 0
};
/*
var GRID_DIST_FULL_X = 340;
var GRID_DIST_FULL_Y = 201;
var GRID_DIST_MEDIUM_X = 250;
var GRID_DIST_MEDIUM_Y = 88;
var GRID_DIST_SMALL_X = 100;
var GRID_DIST_SMALL_Y = 100;
*/
/*
  Migrations: multiply my_modules (x by 32, y by 16)
  UPDATE my_modules SET x=x*32, y=y*16
  themodel.connection.execute("UPDATE my_modules SET x=x*32, y=y*16")
*/
var GRID_DIST_FULL_X = 10;
var GRID_DIST_FULL_Y = 10;
var GRID_DIST_MEDIUM_X = 7.3;
var GRID_DIST_MEDIUM_Y = 4;
var GRID_DIST_SMALL_X = 2.4;
var GRID_DIST_SMALL_Y = 5;
var SUBMIT_FORM_NAME_SEPARATOR = "|";

//************************************
// GLOBAL VARIABLES
//************************************

// Current GUI mode
var currentMode = "full_zoom";

// JSNetworkX graph structure, used for graph analysis
var graph;

// Instance of jsPlumb, a library for canvas manipulation
var instance;

// ID "generator" for new modules
var newModuleIndex = 0;

// Global variables for module dragging
var leftInitial = 0, topInitial = 0, collided = false;

// Global variables for canvas dragging
var x_start = 0, y_start = 0;
var drag_type = DRAG_INVALID;
var draggable = null;

// Draggable position (initial values specified here)
var draggableLeft = 0.5;
var draggableTop = 0.5;

var ignoreUnsavedWorkAlert;

// Global variable for hammer js
var hammertime;

/*
 * As a guideline, all module elements should contain
 * the following attributes:
 *
 * id - ID of the module.
 * data-module-name - Name of the module.
 * data-module-id - ID of the module.
 * data-module-group - ID of the group the module belongs to (if it exists).
 * data-module-x - X position of the module (integer).
 * data-module-y - Y position of the module (integer).
 * data-module-conns - List of module IDs this module is connected
 * to (outbound connections).
 */

//************************************
// DEFAULT INITIALIZATION CODE
//************************************

/**
 * Initializes page
 */
function init() {
  bindModeChange();
  bindAjax();
  bindWindowResizeEvent();
  initializeGraph(".diagram .module-large");
  initializeFullZoom();
}

jsPlumb.ready(function () {
  init();
});

//************************************
// INDIVIDUAL ACTION INIT & DESTROY
//************************************

function initializeEdit() {
  newModuleIndex = 0;
  ignoreUnsavedWorkAlert = false;

  // Read permissions from the data attributes of the form
  var canEditModules = _.isEqual($("#update-canvas").data("can-edit-modules"), "yes");
  var canCreateModules = _.isEqual($("#update-canvas").data("can-create-modules"), "yes");
  var canCloneModules = _.isEqual($("#update-canvas").data("can-clone-modules"), "yes");
  var canMoveModules = _.isEqual($("#update-canvas").data("can-move-modules"), "yes");
  var canDeleteModules = _.isEqual($("#update-canvas").data("can-delete-modules"), "yes");
  var canDragModules = _.isEqual($("#update-canvas").data("can-reposition-modules"), "yes");
  var canEditConnections = _.isEqual($("#update-canvas").data("can-edit-connections"), "yes");

  $("#canvas-container").addClass("canvas-container-edit-mode");

  // Hide sidebar & also its toggle button
  $("#wrapper").addClass("hidden2");
  $(".navbar-secondary").addClass("navbar-without-sidebar");
  $("#sidebar-arrow").addClass("invisible");

  // Also, hide zoom levels button group
  $("#diagram-buttons").hide();

  // Resize container
  resizeContainer();

  positionModules(".diagram .module", GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y);
  initJsPlumb(
    "#diagram-container",
    "#diagram",
    "div.module",
    {
      scrollEnabled: true,
      gridDistX: GRID_DIST_EDIT_STEP_X,
      gridDistY: GRID_DIST_EDIT_STEP_Y,
      endpointStyle: EDIT_ENDPOINT_STYLE,
      connectorStyle2: EDIT_CONNECTOR_STYLE_2,
      zoomEnabled: true,
      modulesDraggable: canDragModules,
      connectionsEditable: canEditConnections
    }
  );
  bindEditModeDropdownHandlers();
  if (canCreateModules) {
    bindNewModuleAction(GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y);
  }
  bindEditFormSubmission(GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y);

  if (canEditModules) {
    initEditModules();
    $(".edit-module").on("click touchstart", editModuleHandler);
  }

  if (canCloneModules) {
    bindCloneModuleAction(
      $(".module-options a.clone-module"),
      ".diagram .module",
      GRID_DIST_EDIT_X,
      GRID_DIST_EDIT_Y);
    bindCloneModuleGroupAction(
      $(".module-options a.clone-module-group"),
      ".diagram .module",
      GRID_DIST_EDIT_X,
      GRID_DIST_EDIT_Y);
  }
  if (canMoveModules) {
    initMoveModules();
    $(".move-module").on("click touchstart", moveModuleHandler);

    initMoveModuleGroups();
    $(".move-module-group").on("click touchstart", moveModuleGroupHandler);
  }
  if (canDeleteModules) {
    bindDeleteModuleAction();
    bindDeleteModuleGroupAction();
  }

  bindEditModeCloseWindow();
  bindTouchDropdowns($(".dropdown-toggle"));

  // Restore draggable position
  restoreDraggablePosition($("#diagram"), $("#canvas-container"));

  preventCanvasReloadOnSave();
  $("#canvas-container").submit(function (){
    animateSpinner(
      this,
      true,
      { color: 'white', shadow: true }
    );
  });
  $.initTooltips();
}

function destroyEdit() {
  // Read permissions from the data attributes of the form
  var canCreateModules = _.isEqual($("#update-canvas").data("can-create-modules"), "yes");
  var canCloneModules = _.isEqual($("#update-canvas").data("can-clone-modules"), "yes");
  var canMoveModules = _.isEqual($("#update-canvas").data("can-move-modules"), "yes");
  var canDeleteModules = _.isEqual($("#update-canvas").data("can-delete-modules"), "yes");

  instance.cleanupListeners();
  $(".dropdown").off("show.bs.dropdown hide.bs.dropdown");
  $("#diagram-container").off("mousewheel mousedown mouseup mousemove");
  hammertime.off('pinch');
  $("form#update-canvas").off("submit");
  if (canDeleteModules) {
    $(".delete-container a").off("click");
    $("#modal-delete-module").off("show.bs.modal hide.bs.modal");
    $("#modal-delete-module").find("button[data-action='confirm']").off("click");

    $(".buttons-container a.delete-module").off("click touchstart");
    $(".buttons-container a.delete-module-group").off("click touchstart");
    $("#modal-delete-module-group").off("show.bs.modal hide.bs.modal");
    $("#modal-delete-module-group").find("button[data-action='confirm']").off("click");
  }
  if (canCreateModules) {
    $("#modal-new-module").off("show.bs.modal shown.bs.modal hide.bs.modal");
    $("#modal-new-module").find("button[data-action='confirm']").off("click");
    $("#canvas-new-module").draggable("destroy");
    $("#canvas-new-module").off("click");
  }
  if (canCloneModules) {
    $(".buttons-container a.clone-module").off("click touchstart");
    $(".buttons-container a.clone-module-group").off("click touchstart");
  }
  if (canMoveModules) {
    $(".move-module").off("click touchstart");
    $(".move-module-group").off("click touchstart");
  }

  $("#update-canvas .cancel-edit-canvas").off("click");
  $(window).off("beforeunload");
  $(document).off("page:before-change");
  $(".dropdown-toggle").off("touchstart");

  // Remember the draggable position
  rememberDraggablePosition($("#diagram"), $("#canvas-container"));
}

function initializeFullZoom() {
  // Resize container
  resizeContainer();

  positionModules(".diagram .module-large", GRID_DIST_FULL_X, GRID_DIST_FULL_Y);
  initJsPlumb(
    "#diagram-container",
    "#diagram",
    "div.module-large",
    {
      scrollEnabled: true,
      gridDistX: GRID_DIST_FULL_X,
      gridDistY: GRID_DIST_FULL_Y
    });
  bindEditDueDateAjax();
  bindEditTagsAjax($("div.module-large"));
  bindFullZoomAjaxTabs();
  initModulesHover($("div.module-large"), $("#slide-panel"));
  initSidebarClicks($("div.module-large"), $("#slide-panel"), $("#diagram"), $("#canvas-container"), 20);

  // Restore draggable position
  restoreDraggablePosition($("#diagram"), $("#canvas-container"));

  // Initialize comments
  Comments.initCommentOptions("ul.content-comments", false);
  Comments.initEditComments(".panel.module-large .tab-content");
  Comments.initDeleteComments(".panel.module-large .tab-content");
}

function destroyFullZoom() {
  instance.cleanupListeners();
  $("#diagram-container").off("mousedown mouseup mousemove");
  $(".module-large .buttons-container [role=tab]").off("ajax:before ajax:success ajax:error");
  $("div.module-large").off("mouseenter mouseleave");
  $("div.module-large a.due-date-link").off("ajax:success ajax:error");
  $("#manage-module-due-date-modal [data-action='submit']").off("click");
  $("div.module-large a.edit-tags-link").off("ajax:before ajax:success");
  $("li[data-module-group]").off("mouseenter mouseleave");
  $("li[data-module-group] > span > a.canvas-center-on").off("click");
  $("li[data-module-id]").off("mouseenter mouseleave");
  $("li[data-module-id] > span > a.canvas-center-on").off("click");

  // Clean up comments listeners
  $(document).off("scroll");
  $(".panel.module-large .tab-content")
  .off("click", "[data-action=edit-comment]");
  $(".panel.module-large .tab-content")
  .off("click", "[data-action=delete-comment]");

  // Remember the draggable position
  rememberDraggablePosition($("#diagram"), $("#canvas-container"));
}

function initializeMediumZoom() {
  // Resize container
  resizeContainer();

  positionModules(".diagram .module-medium", GRID_DIST_MEDIUM_X, GRID_DIST_MEDIUM_Y);
  initJsPlumb("#diagram-container", "#diagram", "div.module-medium", { scrollEnabled: true, gridDistX: GRID_DIST_MEDIUM_X, gridDistY: GRID_DIST_MEDIUM_Y });
  bindEditTagsAjax($("div.module-medium"));
  initModulesHover($("div.module-medium"), $("#slide-panel"));
  initSidebarClicks($("div.module-medium"), $("#slide-panel"), $("#diagram"), $("#canvas-container"), 20);

  // Restore draggable position
  restoreDraggablePosition($("#diagram"), $("#canvas-container"));
}

function destroyMediumZoom() {
  instance.cleanupListeners();
  $("#diagram-container").off("mousedown mouseup mousemove");
  $("div.module-medium").off("mouseenter mouseleave");
  $("div.module-medium a.edit-tags-link").off("ajax:before ajax:success");
  $("li[data-module-group]").off("mouseenter mouseleave");
  $("li[data-module-group] > span > a.canvas-center-on").off("click");
  $("li[data-module-id]").off("mouseenter mouseleave");
  $("li[data-module-id] > span > a.canvas-center-on").off("click");

  // Remember the draggable position
  rememberDraggablePosition($("#diagram"), $("#canvas-container"));
}

function initializeSmallZoom() {
  // Resize container
  resizeContainer();

  positionModules(".diagram .module-small", GRID_DIST_SMALL_X, GRID_DIST_SMALL_Y);
  initJsPlumb("#diagram-container", "#diagram", "div.module-small", { scrollEnabled: true, gridDistX: GRID_DIST_SMALL_X, gridDistY: GRID_DIST_SMALL_Y });
  initModulesHover($("div.module-small"), $("#slide-panel"));
  initSidebarClicks($("div.module-small"), $("#slide-panel"), $("#diagram"), $("#canvas-container"), 20);

  // Restore draggable position
  restoreDraggablePosition($("#diagram"), $("#canvas-container"));
}

function destroySmallZoom() {
  instance.cleanupListeners();
  $("#diagram-container").off("mousedown mouseup mousemove");
  $("div.module-small").off("mouseenter mouseleave");
  $("li[data-module-group]").off("mouseenter mouseleave");
  $("li[data-module-group] > span > a.canvas-center-on").off("click");
  $("li[data-module-id]").off("mouseenter mouseleave");
  $("li[data-module-id] > span > a.canvas-center-on").off("click");

  // Remember the draggable position
  rememberDraggablePosition($("#diagram"), $("#canvas-container"));
}

//************************************
// FUNCTIONS
//************************************

/**
 * Enable/disable canvas events (related to dragging, zooming, ...).
 * @param activate - True to activate events; false
 * to deactivate them.
 */

var canHammer = function(recognizer, event) {
    event = event || window.event;
    if (event==null) {
      return false;
    }
    var result = (event.target instanceof HTMLInputElement && event.target.type == 'text');
    return !result;
  }


function toggleCanvasEvents(activate) {
  var cmd = "pause";
  if (activate) {
    cmd = "active";
  }
  $("#diagram-container").eventPause(cmd,
    "mousedown mouseup mouseout mousewheel touchstart touchend touchcancel touchmove");
  hammertime.get('pinch').set({ enable: canHammer });
}

/**
 * Gets or sets the left CSS position of the element.
 * @param el - The element.
 * @param newVal - The new left CSS value, if setting value.
 * @return The new float value of the element's left CSS position.
 */
function elLeft(el, newVal) {
  if (_.isUndefined(newVal)) {
    return parseFloat($(el).css("left").replace("px", ""));
  } else {
    $(el).css("left", newVal + "px");
    return newVal;
  }
}

/**
 * Gets or sets the top CSS position of the element.
 * @param el - The element.
 * @param newVal - The new top CSS value, if setting value.
 * @return The new float value of the element's top CSS position.
 */
function elTop(el, newVal) {
  if (_.isUndefined(newVal)) {
    return parseFloat($(el).css("top").replace("px", ""));
  } else {
    $(el).css("top", newVal + "px");
    return newVal;
  }
}

/**
 * Animate the reposition of the specified element.
 * @param el - The element to be repositioned.
 * @param left - The new left CSS property.
 * @param top - The new top CSS property.
 */
function animateReposition(el, left, top) {
  var leftMove, topMove, leftDir, topDir;
  if (_.isUndefined($(el).css("left"))) {
    leftMove = left;
  } else {
    leftMove = (-parseInt($(el).css("left").replace("px", ""), 10) + left);
  }
  if (_.isUndefined($(el).css("top"))) {
    topMove = top;
  } else {
    topMove = (-parseInt($(el).css("top").replace("px", ""), 10) + top);
  }
  leftDir = leftMove >=0 ? "+=" : "-=";
  topDir = topMove >=0 ? "+=" : "-=";
  el.animate({
    left: leftDir + Math.abs(leftMove) + "px",
    top: topDir + Math.abs(topMove) + "px"
  }, 300);
}

/**
 * Bind the change of the canvas mode.
 */
function bindModeChange() {
  var buttons = $('#diagram-buttons').find("a[type='button']");

  buttons.on('click', function() {
    var action = $(this).data("action");

    // Ignore clicks on the currently active button
    if (_.isEqual(action, currentMode)) {
      return false;
    }

    // Else, call destroy action function
    switch (action) {
      case "edit":
        destroyEdit();
        break;
      case "full_zoom":
        destroyFullZoom();
        break;
      case "medium_zoom":
        destroyMediumZoom();
        break;
      case "small_zoom":
        destroySmallZoom();
        break;
    }
  });
}

function bindTouchDropdowns(selector) {
  selector.on("touchstart", function(event) {
    event.stopPropagation();
  });
}

function bindEditModeCloseWindow() {
  var alertText = $("#update-canvas").attr("data-unsaved-work-text");

  $("#update-canvas .cancel-edit-canvas").click(function(ev) {
    ignoreUnsavedWorkAlert = true;
  });
  $(window).on("beforeunload", function(ev) {
    if (ignoreUnsavedWorkAlert) {
      // Remove unload listeners
      $(window).off("beforeunload");
      $(document).off("page:before-change");

      ev.returnValue = undefined;
      return undefined;
    } else {
      return alertText;
    }
  });
  $(document).on("page:before-change", function(ev) {
    var exit;
    if (ignoreUnsavedWorkAlert) {
      exit = true;
    } else {
      exit = confirm(alertText);
    }

    if (exit) {
      // Remove unload listeners
      $(window).off("beforeunload");
      $(document).off("page:before-change");
    }

    return exit;
  });
}

function bindEditModeDropdownHandlers(node) {
  // When "module clone/delete" dropdowns are opened,
  // module needs to increase z-index in order for the dropdown
  // menu to be above connections etc.
  $(".dropdown", node).on("show.bs.dropdown", function(event) {
    $(this).parents(".module").css("z-index", "30");
  });
  $(".dropdown", node).on("hide.bs.dropdown", function(event) {
    $(this).parents(".module").css("z-index", "20");
  });
}

function resizeContainer() {
  // Resize diagram container
  var cont = $("#diagram-container");

  if (cont.length > 0) {
    cont.css(
      "height",
      ($(window).height() - cont.offset().top - 15) + "px"
    );
  }
}

function bindWindowResizeEvent() {
  $(window).resize(function() {
    resizeContainer();
  });
}

function bindFullZoomAjaxTabs() {
  var manageUsersModal = null;
  var manageUsersModalBody = null;

  // Initialize users editing modal remote loading.
  function initUsersEditLink($el) {
     $el.find(".manage-users-link")
       .on("ajax:before", function () {
          var moduleId = $(this).closest(".panel-default").attr("data-module-id");
          manageUsersModal.attr("data-module-id", moduleId);
          manageUsersModal.modal('show');
       })
       .on("ajax:success", function (e, data) {
         $("#manage-module-users-modal-module").text(data.my_module.name);
         initUsersModalBody(data);
       });
  }

  // Initialize reloading manage user modal content after posting new
  // user.
  function initAddUserForm() {
    manageUsersModalBody.find(".add-user-form")
      .on("ajax:success", function (e, data) {
        initUsersModalBody(data);
      });
  }

  // Initialize remove user from my_module links.
  function initRemoveUserLinks() {
    manageUsersModalBody.find(".remove-user-link")
      .on("ajax:success", function (e, data) {
        initUsersModalBody(data);
      });
  }

  // Initialize ajax listeners and elements style on modal body. This
  // function must be called when modal body is changed.
  function initUsersModalBody(data) {
    manageUsersModalBody.html(data.html);
    manageUsersModalBody.find(".selectpicker").selectpicker();
    initAddUserForm();
    initRemoveUserLinks();
  }

  manageUsersModal = $("#manage-module-users-modal");
  manageUsersModalBody = manageUsersModal.find(".modal-body");

  // Reload users tab HTML element when modal is closed
  manageUsersModal.on("hide.bs.modal", function () {
    var moduleEl = $("div[data-module-id='" +
                     $(this).attr("data-module-id") + "']");
    // Load HTML to refresh users list
    $.ajax({
      url: moduleEl.attr("data-module-users-tab-url"),
      type: "GET",
      dataType: "json",
      success: function (data) {
        moduleEl.find("#" + moduleEl.attr("data-module-id") + "_users").html(data.html);
        CounterBadge.updateCounterBadge(data.counter,
                                        data.my_module_id, 'users');
        initUsersEditLink(moduleEl);
      },
      error: function (data) {
        // TODO
      }
    });
  });

  // Remove users modal content when modal window is closed.
  manageUsersModal.on("hidden.bs.modal", function () {
    manageUsersModalBody.html("");
  });

  // initialize my_module tab remote loading
  var elements = $(".module-large .buttons-container [role=tab]");
  elements.on("ajax:before", function (e) {
    var $this = $(this);
    var parentNode = $this.parents("li");
    var targetId = $this.attr("aria-controls");

    if (parentNode.hasClass("active")) {
      parentNode.removeClass("active");
      $("#" + targetId).removeClass("active");
      $this.parents(".module-large").addClass("expanded");
      return false;
    }
  })
  .on("ajax:success", function (e, data, status, xhr) {

    // Hide all potentially shown tabs
    elements.parents("li").removeClass("active");
    $(".tab-content").children().removeClass("active");
    $(".module-large").removeClass("expanded");

    var $this = $(this);
    var targetId = $this.attr("aria-controls");
    var target = $("#" + targetId);
    var targetContents = target.attr("data-contents");
    var parentNode = $this.parents("ul").parent();

    target.html(data.html);
    if (targetContents === "info") {
      initEditDescription(parentNode);
    } else if (targetContents === "users") {
      initUsersEditLink(parentNode);
    } else if (targetContents === "comments") {
      Comments.form(parentNode);
      Comments.moreComments(parentNode);
    }

    $this.parents("ul").parent().find(".active").removeClass("active");
    $this.parents("li").addClass("active");
    target.addClass("active");
    $this.parents(".module-large").addClass("expanded");

    // Call scrollBotton after the comments are displayed
    // so that the scrollHight can be calculated
    if ( targetContents === 'comments' ) {
      Comments.scrollBottom(parentNode);
    }
  })
  .on("ajax:error", function (e, xhr, status, error) {
    // TODO
  });
}

function bindEditDueDateAjax() {
  var editDueDateModal = null;
  var editDueDateModalBody = null;
  var editDueDateModalTitle = null;
  var editDueDateModalSubmitBtn = null;

  editDueDateModal = $("#manage-module-due-date-modal");
  editDueDateModalBody = editDueDateModal.find(".modal-body");
  editDueDateModalTitle = editDueDateModal.find("#manage-module-due-date-modal-label");
  editDueDateModalSubmitBtn = editDueDateModal.find("[data-action='submit']");

  $("div.module-large .panel-body .due-date-link")
  .on("ajax:success", function(ev, data, status) {
    var dueDateLink = $(this);
    if (!dueDateLink.hasClass("due-date-refresh")) {
      dueDateLink = dueDateLink.parent().next().find(".due-date-refresh");
    }
    var moduleEl = dueDateLink.closest("div.module-large");

    // Load contents
    editDueDateModalBody.html(data.html);
    editDueDateModalTitle.text(data.title);

    // Add listener to form inside modal
    editDueDateModalBody.find("form")
    .on("ajax:success", function(ev2, data2, status2) {
      // Update module's due date
      dueDateLink.html(data2.due_date_label);

      // Update module's classes if needed
      moduleEl
      .removeClass("alert-red")
      .removeClass("alert-yellow");
      _.each(data2.alerts, function(alert) {
        moduleEl.addClass(alert);
      });

      // Close modal
      editDueDateModal.modal("hide");
    })
    .on("ajax:error", function(ev2, data2, status2) {
      // Display errors if needed
      $(this).renderFormErrors("my_module", data2.responseJSON);
    });

    // Disable canvas dragging events
    toggleCanvasEvents(false);

    // Open modal
    editDueDateModal.modal("show");
  })
  .on("ajax:error", function(ev, data, status) {
    // TODO
  });

  editDueDateModalSubmitBtn.on("click", function() {
    // Submit the form inside the modal
    editDueDateModalBody.find("form").submit();
  });

  editDueDateModal.on("hidden.bs.modal", function() {
    editDueDateModalBody.find("form").off("ajax:success ajax:error");
    editDueDateModalBody.html("");

    // Re-activate canvas dragging events
    toggleCanvasEvents(true);
  });
}

function bindEditTagsAjax(elements) {
  var manageTagsModal = null;
  var manageTagsModalBody = null;

  // Initialize reloading of manage tags modal content after posting new
  // tag.
  function initAddTagForm() {
    manageTagsModalBody.find(".add-tag-form")
      .on("ajax:success", function (e, data) {
        initTagsModalBody(data);
      });
  }

  // Initialize edit tag & remove tag functionality from my_module links.
  function initTagRowLinks() {
    manageTagsModalBody.find(".edit-tag-link")
      .on("click", function (e) {
        var $this = $(this);
        var li = $this.parents("li.list-group-item");
        var editDiv = $(li.find("div.tag-edit"));

        // Revert all rows to their original states
        manageTagsModalBody.find("li.list-group-item").each(function(){
          var li = $(this);
          li.css("background-color", li.data("color"));
          li.find(".edit-tag-form").clearFormErrors();
          li.find("input[type=text]").val(li.data("name"));
        });

        // Hide all other edit divs, show all show divs
        manageTagsModalBody.find("div.tag-edit").hide();
        manageTagsModalBody.find("div.tag-show").show();

        editDiv.find("input[type=text]").val(li.data("name"));
        editDiv.find('.edit-tag-color').colorselector('setColor', li.data("color"));

        li.find("div.tag-show").hide();
        editDiv.show();
      });
    manageTagsModalBody.find("div.tag-edit .dropdown-colorselector > .dropdown-menu li a")
      .on("click", function (e) {
        // Change background of the <li>
        var $this = $(this);
        var li = $this.parents("li.list-group-item");
        li.css("background-color", $this.data("value"));
      });
    manageTagsModalBody.find(".remove-tag-link")
      .on("ajax:success", function (e, data) {
        initTagsModalBody(data);
      });
      manageTagsModalBody.find(".delete-tag-form")
      .on("ajax:success", function (e, data) {
        initTagsModalBody(data);
      });
    manageTagsModalBody.find(".edit-tag-form")
      .on("ajax:success", function (e, data) {
        initTagsModalBody(data);
      })
      .on("ajax:error", function (e, data) {
        $(this).renderFormErrors("tag", data.responseJSON);
      });
    manageTagsModalBody.find(".cancel-tag-link")
      .on("click", function (e, data) {
        var $this = $(this);
        var li = $this.parents("li.list-group-item");

        li.css("background-color", li.data("color"));
        li.find(".edit-tag-form").clearFormErrors();

        li.find("div.tag-edit").hide();
        li.find("div.tag-show").show();
      });
  }

  // Initialize ajax listeners and elements style on modal body. This
  // function must be called when modal body is changed.
  function initTagsModalBody(data) {
    manageTagsModal.data('module-id', data.my_module.id)
    manageTagsModalBody.html(data.html);
    manageTagsModalBody.find(".selectpicker").selectpicker();
    initAddTagForm();
    initTagRowLinks();
  }

  manageTagsModal = $("#manage-module-tags-modal");
  manageTagsModalBody = manageTagsModal.find(".modal-body");

  // Reload tags HTML element when modal is closed
  manageTagsModal.on("hide.bs.modal", function(){
    var task = $("div.panel[data-module-id='" +
                 manageTagsModal.data('module-id') + "']");

    // Load HTML
    $.ajax({
      url: task.attr("data-module-tags-url"),
      type: "GET",
      dataType: "json",
      success: function(data){
        task.find(".edit-tags-link")
        .html(data.html_canvas);
      },
      error: function(data){
        // TODO
      }
    });
  });

  // Remove modal content when modal window is closed.
  manageTagsModal.on("hidden.bs.modal", function () {
    manageTagsModalBody.html("");
  });

  // initialize my_module tab remote loading
  $(elements).find("a.edit-tags-link")
  .on("ajax:before", function () {
    var moduleId = $(this).closest(".panel-default").attr("data-module-id");
    manageTagsModal.attr("data-module-id", moduleId);
    manageTagsModal.modal('show');
  })
  .on("ajax:success", function (e, data) {
    $("#manage-module-tags-modal-module").text(data.my_module.name);
    initTagsModalBody(data);
  })
  .on('click', function(){
    $(this).addClass('updated-module-tags');
  });
}

/**
 * Bind change of GUI buttons to Ajax success callback.
 */
function bindAjax() {
  $('#diagram-buttons .ajax').on('ajax:success', function(evt, data) {
    // Set toggled button state
    $("#diagram-buttons a").removeClass("active");
    $("#diagram-buttons a").removeAttr("aria-pressed");
    $("#diagram-buttons a").removeData("toggle");
    $(evt.target).addClass("active");
    $(evt.target).attr("aria-pressed", true);
    $(evt.target).data("toggle", "button");

    // Fill contents of container with AJAX content
    var target = $('#canvas-container');
    $(target).html(data);

    // Re-run canvas GUI initialization code
    var action = $(evt.target).data("action");
    switch (action) {
      case "edit":
        initializeEdit();
        break;
      case "full_zoom":
        initializeFullZoom();
        break;
      case "medium_zoom":
        initializeMediumZoom();
        break;
      case "small_zoom":
        initializeSmallZoom();
        break;
    }

    currentMode = action;
  });
  $('#diagram-buttons .ajax').on('ajax:error', function(evt, data) {
    // Redirect to provided URL
    var json = $.parseJSON(data.responseText);
    $(location).attr('href', json.redirect_url);
  });
}

/**
 * Add a new node to the graph.
 * @param moduleId - The ID of the module to add.
 * @param module - The module jQuery element.
 */
function addNode(moduleId, module) {
  var connsAttr = module.attr("data-module-conns");
  var conns = _.isUndefined(connsAttr) ? [] : connsAttr.split(", ");
  graph.addNode(
    moduleId,
    {
      name: module.data["module-name"],
      x: module.data["module-x"],
      y: module.data["module-y"],
      conns: conns
    }
  );
}

/**
 * Initialize the global graph variable from modules.
 * @param modulesSel - The jQuery selector text of module elements.
 */
function initializeGraph(modulesSel) {
  var modules = $(modulesSel);

  graph = new jsnx.DiGraph();

  var module, moduleId;
  _.each(modules, function(m) {
    module = $(m);
    moduleId = module.attr("data-module-id");
    if (!graph.hasNode(moduleId)) {
      addNode(moduleId, module);
    }
    var outs = module.attr("data-module-conns").split(", ");
    _.each(outs, function(targetId) {
      if (targetId === "") {
        return;
      }
      if (!graph.hasNode(targetId)) {
        addNode(targetId, $(".diagram .module[data-module-id=" + targetId + "]"));
      }

      graph.addEdge(module.attr("data-module-id"), targetId);
    });
  });
}

/**
 * Get the connected components of a specified graph and module. Alas, this
 * function doesn't exist in jsnetworkx.
 * @param graph - The graph instance.
 * @param moduleId - We're only interested in the connected component in which
 * the specified module is located.
 * @return A list of node IDs representing a connected component.
 */
function connectedComponents(graph, moduleId) {
  function getNeighbors(graph, node, visited) {
    visited.push(node);
    var neighbours = _.union(graph.predecessors(node), graph.successors(node));
    var unvisitedNeighbors = _.filter(neighbours, function(n) {
      return !_.contains(visited, n);
    });
    var result = _.flatten(_.map(unvisitedNeighbors, function(neighbour) {
      nodes = getNeighbors(graph, neighbour, visited);
      _.each(nodes, function(n) {
        if (!_.contains(visited, n)) {
          visited.push(n);
        }
      });
      return nodes;
    }));
    result.push(node);
    return result;
  }

  return _.uniq(getNeighbors(graph, moduleId, []));
}

/**
 * Create a virtual new module (without links & functionality).
 * @param event - The event, can be null.
 */
function createVirtualModule(event) {
  // Generate new module div
  var newModule = document.createElement("div");
  $(newModule)
  .addClass("panel panel-default module new")
  .css("z-index", "900")
  .attr("data-module-name", "")
  .attr("data-module-group-name","")
  .attr("data-module-x", "")
  .attr("data-module-y", "")
  .attr("data-module-conns", "")
  .appendTo(draggable);

  var panelHeading = document.createElement("div");
  $(panelHeading)
  .addClass("panel-heading")
  .appendTo($(newModule));

  var panelTitle = document.createElement("div");
  $(panelTitle)
  .addClass("panel-title")
  .html("")
  .appendTo($(panelHeading));

  if (_.isEqual($("#update-canvas").data("can-edit-connections"), "yes")) {
    var panelBody = document.createElement("div");
    $(panelBody)
    .addClass("panel-body ep")
    .appendTo($(newModule));
  }

  var overlayContainer = document.createElement("div");
  $(overlayContainer)
  .addClass("overlay")
  .appendTo($(newModule));

  return $(newModule);
}

/**
 * Update a previously created virtual module with HTML elements.
 * @param module - The jQuery module selector.
 * @param id - The new module id.
 * @param name - The module name.
 * @param gridDistX - The grid distance in X direction.
 * @param gridDistY - The grid distance in Y direction.
 * @return The updated module.
 */
function updateModuleHtml(module, id, name, gridDistX, gridDistY) {
  // Update some stuff inside the module
  module
  .attr("id", id)
  .attr("data-module-id", id)
  .attr("data-module-name", name)
  .css("z-index", 20);

  var panelHeading = module.find(".panel-heading");

  module.find(".panel-title").html(name);

  module.find(".ep").html($("#drag-connections-placeholder").text().trim());

  // Add dropdown
  var dropdown = document.createElement("div");
  $(dropdown)
  .addClass("dropdown pull-right module-options")
  .appendTo($(panelHeading));

  var dropdownToggle = document.createElement("a");
  $(dropdownToggle)
  .addClass("dropdown-toggle")
  .attr("id", id + "_options")
  .attr("data-toggle", "dropdown")
  .attr("aria-haspopup", "true")
  .attr("aria-expanded", "true")
  .appendTo(dropdown);

  var toggleIcon = document.createElement("span");
  $(toggleIcon)
  .addClass("fas")
  .addClass("fa-caret-down")
  .attr("aria-hidden", "true")
  .appendTo(dropdownToggle);

  var dropdownMenu = document.createElement("ul");
  $(dropdownMenu)
  .addClass("dropdown-menu")
  .addClass("custom-dropdown-menu")
  .addClass("no-scale")
  .attr("aria-labelledby", id + "_options")
  .appendTo(dropdown);

  var dropdownMenuHeader = document.createElement("li");
  $(dropdownMenuHeader)
  .addClass("dropdown-header")
  .html($("#dropdown-header-placeholder").text().trim())
  .appendTo(dropdownMenu);

  // Add edit links if neccesary
  if (_.isEqual($("#update-canvas").data("can-edit-modules"), "yes")) {
    var editModuleItem = document.createElement("li");
    $(editModuleItem).appendTo(dropdownMenu);

    var editModuleLink = document.createElement("a");
    $(editModuleLink)
    .attr("href", "")
    .attr("data-module-id", id)
    .addClass("edit-module")
    .html($("#edit-link-placeholder").text().trim())
    .appendTo(editModuleItem);

    // Add click handler for the edit module
    $(editModuleLink).on("click touchstart", editModuleHandler);
  }

  // Add clone links if neccesary
  if (_.isEqual($("#update-canvas").data("can-clone-modules"), "yes")) {
    var cloneModuleItem = document.createElement("li");
    $(cloneModuleItem).appendTo(dropdownMenu);

    var cloneModuleLink = document.createElement("a");
    $(cloneModuleLink)
    .attr("href", "")
    .attr("data-module-id", id)
    .addClass("clone-module")
    .html($("#clone-link-placeholder").text().trim())
    .appendTo(cloneModuleItem);

    // Add clone click handler for the new module
    bindCloneModuleAction($(cloneModuleLink), ".diagram .module", gridDistX, gridDistY);

    var cloneModuleGroupItem = document.createElement("li");
    $(cloneModuleGroupItem).appendTo(dropdownMenu);
    $(cloneModuleGroupItem).hide();

    var cloneModuleGroupLink = document.createElement("a");
    $(cloneModuleGroupLink)
    .attr("href", "")
    .attr("data-module-id", id)
    .addClass("clone-module-group")
    .html($("#clone-group-link-placeholder").text().trim())
    .appendTo(cloneModuleGroupItem);

    // Add clone click handler for the new module
    bindCloneModuleGroupAction($(cloneModuleGroupLink), ".diagram .module", gridDistX, gridDistY);

    bindEditModeDropdownHandlers(module);
  }

  // Add move links if neccesary
  if (_.isEqual($("#update-canvas").data("can-move-modules"), "yes")) {
    var moveModuleItem = document.createElement("li");
    $(moveModuleItem).appendTo(dropdownMenu);

    var moveModuleLink = document.createElement("a");
    $(moveModuleLink)
    .attr("href", "")
    .attr("data-module-id", id)
    .addClass("move-module")
    .html($("#move-link-placeholder").text().trim())
    .appendTo(moveModuleItem);

    // Add clone click handler for the new module
    $(moveModuleLink).on("click touchstart", moveModuleHandler);

    // Add buttons for module groups
    var moveModuleGroupItem = document.createElement("li");
    $(moveModuleGroupItem).appendTo(dropdownMenu);
    $(moveModuleGroupItem).hide();

    var moveModuleGroupLink = document.createElement("a");
    $(moveModuleGroupLink)
    .attr("href", "")
    .attr("data-module-id", id)
    .addClass("move-module-group")
    .html($("#move-group-link-placeholder").text().trim())
    .appendTo(moveModuleGroupItem);

    $(moveModuleGroupLink).on("click touchstart", moveModuleGroupHandler);
  }

  // Add delete links if neccesary
  if (_.isEqual($("#update-canvas").data("can-delete-modules"), "yes")) {
    var deleteModuleItem = document.createElement("li");
    $(deleteModuleItem).appendTo(dropdownMenu);

    var deleteModuleLink = document.createElement("a");
    $(deleteModuleLink)
    .attr("href", "")
    .attr("data-module-id", id)
    .addClass("delete-module")
    .html($("#delete-link-placeholder").text().trim())
    .appendTo(deleteModuleItem);

    // Add delete click handler for the new module
    $(deleteModuleLink).on("click touchstart", deleteModuleHandler);

    var deleteModuleGroupItem = document.createElement("li");
    $(deleteModuleGroupItem).appendTo(dropdownMenu);
    $(deleteModuleGroupItem).hide();

    var deleteModuleGroupLink = document.createElement("a");
    $(deleteModuleGroupLink)
    .attr("href", "")
    .attr("data-module-id", id)
    .addClass("delete-module-group")
    .html($("#delete-group-link-placeholder").text().trim())
    .appendTo(deleteModuleGroupItem);

    // Add delete click handler for the new module
    $(deleteModuleGroupLink).on("click touchstart", deleteModuleGroupHandler);
  }

  // Set it up for jsPlumb, depending on permissions
  if (_.isEqual($("#update-canvas").data("can-reposition-modules"), "yes")) {
    addDraggablesToInstance(module, gridDistX, gridDistY);
  }

  if (_.isEqual($("#update-canvas").data("can-edit-connections"), "yes")) {
    setElementsAsDropTargets(module);
    setElementsAsDragSources(module, null, null, EDIT_CONNECTOR_STYLE_2);
  }


  // Add dropdown touch support
  bindTouchDropdowns($(dropdownToggle));

  // Re-zoom dropdown menu, so the new module's no-scale dropdown gets
  // rescaled
  $(dropdownMenu).css("transform", "scale(" + (1.0 / instance.getZoom()) + ")");
  $(dropdownMenu).css("transform-origin", "0 0");

  // Add IDs to the form
  var formAddInput = $('#update-canvas form input#add');
  var formAddNameInput = $('#update-canvas form input#add-names');
  var inputVal = formAddInput.attr("value");
  var inputNameVal = formAddNameInput.attr("value");
  if (_.isUndefined(inputVal) || inputVal === "") {
    formAddInput.attr("value", id);
    formAddNameInput.attr("value", name);
  } else {
    formAddInput.attr("value", inputVal + "," + id);
    formAddNameInput.attr("value", inputNameVal + SUBMIT_FORM_NAME_SEPARATOR + name);
  }

  return module;
}

/**
 * Bind the new module button action.
 * @param gridDistX - The canvas grid distance in X direction.
 * @param gridDistY - The canvas grid distance in Y direction.
 */
function bindNewModuleAction(gridDistX, gridDistY) {
  function handleDragStart(event, ui) {
    collided = false;
  }

  function handleDrag(event, ui) {
    // Custom grid implementation is needed
    // (so the new module snaps on the same grid offset
    // as the other modules)
    var l = ui.position.left;
    var t = ui.position.top;
    var gdx = gridDistX;
    var gdy = gridDistY;
    var z = instance.getZoom();

    ui.position.left = Math.floor(l / (gdx * z)) * gdx;
    ui.position.top = Math.floor(t / (gdy * z)) * gdy;

    // Check if collision occured
    var modules = $(".module");
    var module;
    for (var i = 0; i < modules.length; i++) {
      module = $(modules[i]);
      if (module.hasClass("new")) {
        continue;
      }

      if (_.isEqual(ui.helper.position(), module.position())) {
        // Collision!
        collided = true;
        break;
      } else {
        collided = false;
      }
    }

    if (collided) {
      ui.helper.addClass("collided");
    } else {
      ui.helper.removeClass("collided");
    }
  }

  function handleDragStop(event, ui) {
    if (!collided) {
      // Disable scroll on canvas temporarily, as it can be
      // dragged from modal area
      toggleCanvasEvents(false);

      // Copy the ui.helper, since it's gonna vanish soon!
      var clone = ui.helper.clone(true, true);
      clone.appendTo(draggable);
      clone.css("z-index", "20");

      // Open modal window
      $("#modal-new-module").modal({
        "backdrop": "static"
      });
    }

    collided = false;
  }

  function handleNewNameConfirm(ev) {
    var input = $("#new-module-name-input");
    // Validate module name
    var moduleNameValid = textValidator(ev, input,
      <%= Constants::NAME_MIN_LENGTH %>, <%= Constants::NAME_MAX_LENGTH %>,
      true);
    if (moduleNameValid) {
      // Set the "clicked" property to true
      modal.data("submit", "true");
    }
    return moduleNameValid;
  }

  var newModuleBtn = $("#canvas-new-module");
  var modal = $("#modal-new-module");

  newModuleBtn.draggable({
    cursor: "move",
    helper: createVirtualModule,
    start: handleDragStart,
    drag: handleDrag,
    stop: handleDragStop
  });

  // Prevent "new module" button from submitting form
  newModuleBtn.click(function(event) {
    event.preventDefault();
    event.stopPropagation();
    return false;
  });

  // Bind the confirm button on modal
  modal.find("button[data-action='confirm']").on("click", function(ev) {
    handleNewNameConfirm(ev);
  });

  // Also, bind on modal window open & close
  modal.on("show.bs.modal", function(ev) {
    // Clear input
    $(this).removeData("submit");
    $(this).find("#new-module-name-input").val("");

    // Remove potential error classes from form
    $(this).find("#new-module-name-input").parent().removeClass("has-error");
    $(this).find("span.help-block").remove();

    // Bind onto input keypress (to prevent form from being submitted)
    $(this).find("#new-module-name-input").keydown(function(ev) {
      if (ev.keyCode == 13) {
        if (handleNewNameConfirm(ev)) {
          // Close modal
          modal.modal("hide");
        }

        // In any case, prevent form submission
        ev.preventDefault();
        ev.stopPropagation();
        return false;
      }
    });
  });

  modal.on("shown.bs.modal", function(event) {
    // Focus the text element
    $(this).find("#new-module-name-input").focus();
  });

  modal.on("hide.bs.modal", function (event) {
    var newModule = $(".module.new");

    $(this).find("#new-module-name-input").off("keydown");

    if (_.isEqual($(event.target).data("submit"), "true")) {
      // If modal was successfully submitted, generate the module
      var id = "n" + newModuleIndex++;
      graph.addNode(id);
      var name = $(this).find("#new-module-name-input").val();
      updateModuleHtml(newModule, id, name, gridDistX, gridDistY);
      newModule.removeClass("new");
    } else {
      // Else, remove the element
      newModule.remove();
    }

    // In any case, enable scrolling on edit screen again
    toggleCanvasEvents(true);
  });
}

function initEditModules() {

  function handleRenameConfirm(modal, ev) {
    var input = modal.find("#edit-module-name-input");
    // Validate module name
    var moduleNameValid = textValidator(ev, input,
      <%= Constants::NAME_MIN_LENGTH %>, <%= Constants::NAME_MAX_LENGTH %>,
      true);
    if (moduleNameValid) {
      var newName = input.val();
      var moduleId = modal.attr("data-module-id");
      var moduleEl = $("#" + moduleId);
      // Update the module's name in GUI
      moduleEl.attr("data-module-name", newName);
      moduleEl.find(".panel-heading .panel-title").html(newName);

      // Add this information to form
      var formAddInput = $('#update-canvas form input#add');
      var formAddNameInput = $('#update-canvas form input#add-names');
      var formRenameInput = $("#update-canvas form input#rename");
      var addedIds = formAddInput.attr("value").split(",");
      var existingIndex = _.indexOf(addedIds, moduleEl.attr("data-module-id"));
      if (existingIndex === -1) {
        // Actually rename an existing module
        var renameVal = JSON.parse(formRenameInput.attr("value"));
        renameVal[moduleEl.attr("data-module-id")] = newName;
        formRenameInput.attr("value", JSON.stringify(renameVal));
      } else {
        // Just rename the add-name entry
        var addedNames = formAddNameInput.attr("value").split(SUBMIT_FORM_NAME_SEPARATOR);
        addedNames[existingIndex] = newName;
        formAddNameInput.attr("value", addedNames.join(SUBMIT_FORM_NAME_SEPARATOR));
      }

      // Hide modal
      modal.modal("hide");
    }
  }

  $("#modal-edit-module")
  .on("show.bs.modal", function (event) {
    var modal = $(this);
    var moduleId = modal.attr("data-module-id");
    var moduleEl = $("#" + moduleId);
    var input = modal.find("#edit-module-name-input");

    // Set the input to the current module's name
    input.attr("value", moduleEl.attr("data-module-name"));
    input.val(moduleEl.attr("data-module-name"));

    // Bind on enter button
    input.keydown(function(ev) {
      if (ev.keyCode == 13) {
        // "Submit" modal
        handleRenameConfirm(modal, ev);

        // In any case, prevent form submission
        ev.preventDefault();
        ev.stopPropagation();
        return false;
      }
    });
  })
  .on("shown.bs.modal", function(event) {
    // Focus the text element
    $(this).find("#edit-module-name-input").focus();
  })
  .on("hide.bs.modal", function (event) {
    // Remove potential error classes
    $(this).find("#edit-module-name-input").parent().removeClass("has-error");
    $(this).find("span.help-block").remove();

    $(this).find("#edit-module-name-input").off("keydown");

    // When hiding modal, re-enable events
    toggleCanvasEvents(true);
  });

  // Bind the confirm button on modal
  $("#modal-edit-module").find("button[data-action='confirm']").on("click", function(ev) {
    var modal = $(this).closest(".modal");
    handleRenameConfirm(modal, ev);
  });
}

/**
 * Handler when editing a specific module.
 */
editModuleHandler = function(ev) {
  var modal = $("#modal-edit-module");
  var moduleEl = $(this).closest(".module");

  // Set modal's module id
  modal.attr("data-module-id", moduleEl.attr("data-module-id"));

  // Disable dragging & zooming events on canvas temporarily
  toggleCanvasEvents(false);

  // Show modal
  modal.modal("show");

  ev.preventDefault();
  ev.stopPropagation();
  return false;
};

/**
 * Initialize editing of module groups.
 */


/**
 * Handler when editing a module group.
 */

function initMoveModules() {
  function handleMoveConfirm(modal) {
    var moduleId = modal.attr("data-module-id");
    var moduleEl = $("#" + moduleId);
    var input = modal.find('.selectpicker');
    var moveToExperimentId = input.val();

    // Add this information to form
    var formMoveInput = $("#update-canvas form input#move");

    // Save mapping to input
    var moveVal = JSON.parse(formMoveInput.attr("value"));
    moveVal[moduleEl.attr("data-module-id")] = moveToExperimentId;
    formMoveInput.attr("value", JSON.stringify(moveVal));

    updateFormWithModulesData(moduleEl, '', GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y);

    // Delete module from canvas
    deleteModule(moduleEl.attr("data-module-id"), true);

    // Hide modal
    modal.modal("hide");
  }

  $("#modal-move-module")
  .on("show.bs.modal", function (event) {
    var modal = $(this);
    var moduleId = modal.attr("data-module-id");
    var moduleEl = $("#" + moduleId);
    var input = modal.find('.selectpicker');

    // Bind on enter button
    input.keydown(function(ev) {
      if (ev.keyCode == 13) {
        // "Submit" modal
        handleMoveConfirm(modal);

        // In any case, prevent form submission
        ev.preventDefault();
        ev.stopPropagation();
        return false;
      }
    });
  })
  .on("shown.bs.modal", function(event) {
    // Focus the text element
    $(this).find(".selectpicker").selectpicker().focus();
  })
  .on("hide.bs.modal", function (event) {
    // When hiding modal, re-enable events
    toggleCanvasEvents(true);
  });

  // Bind the confirm button on modal
  $("#modal-move-module").find("button[data-action='confirm']").on("click", function(event) {
    var modal = $(this).closest(".modal");
    handleMoveConfirm(modal);
  });
}

/**
 * Handler when trying to move a specific module.
 */
moveModuleHandler = function(ev) {
  var modal = $("#modal-move-module");
  var moduleEl = $(this).closest(".module");

  // Set modal's module id
  modal.attr("data-module-id", moduleEl.attr("data-module-id"));

  // Disable dragging & zooming events on canvas temporarily
  toggleCanvasEvents(false);

  // Show modal
  modal.modal("show");

  ev.preventDefault();
  ev.stopPropagation();
  return false;
};


/**
 * Initialize editing of module groups.
 */
function initMoveModuleGroups() {
  function handleMoveModuleGroupConfirm(modal) {
    var moduleId = modal.attr("data-module-id");
    var moduleEl = $("#" + moduleId);
    var input = modal.find('.selectpicker');
    var moveToExperimentId = input.val();

    // Retrieve all modules in this module group
    var components = connectedComponents(graph, moduleId.toString());
    var group = _.map(components, function(id) { return $("#" + id); });

    var conns = _.filter(graph.edges(), function(conn) {
      return _.contains(components, conn[0]) || _.contains(components, conn[1]);
    });

    updateFormWithModulesData(group, conns.toString(), GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y);

    // Add move information to form
    var formMoveInput = $("#update-canvas form input#move");

    moveModules = [];
    _.each(group, function(m) {
      moveModules.push(m.attr("data-module-id"));
    });

    // Put the array into input
    var moveVal = JSON.parse(formMoveInput.attr("value"));
    moveVal[moveModules] = moveToExperimentId;
    formMoveInput.attr("value", JSON.stringify(moveVal));

    _.each(group, function(m) {
      deleteModule(m.attr("data-module-id"));
    });

    // Hide modal
    modal.modal("hide");
  }

  $("#modal-move-module-group")
  .on("show.bs.modal", function (event) {
    var modal = $(this);
    var moduleId = modal.attr("data-module-id");
    var moduleEl = $("#" + moduleId);
    var input = modal.find('.selectpicker');

    // Bind on enter button
    input.keydown(function(ev) {
      if (ev.keyCode == 13) {
        // "Submit" modal
        handleMoveConfirm(modal);

        // In any case, prevent form submission
        ev.preventDefault();
        ev.stopPropagation();
        return false;
      }
    });
  })
  .on("shown.bs.modal", function(event) {
    // Focus the text element
    $(this).find(".selectpicker").selectpicker().focus();
  })
  .on("hide.bs.modal", function (event) {
    // When hiding modal, re-enable events
    toggleCanvasEvents(true);
  });

  // Bind the confirm button on modal
  $("#modal-move-module-group").find("button[data-action='confirm']").on("click", function(event) {
    var modal = $(this).closest(".modal");
    handleMoveModuleGroupConfirm(modal);
  });
}

/**
 * Handler when editing a module group.
 */
moveModuleGroupHandler = function(ev) {
  var modal = $("#modal-move-module-group");
  var moduleEl = $(this).closest(".module");

  // Set modal's module id
  modal.attr("data-module-id", moduleEl.attr("data-module-id"));

  // Disable dragging & zooming events on canvas temporarily
  toggleCanvasEvents(false);

  // Show modal
  modal.modal("show");

  ev.preventDefault();
  ev.stopPropagation();
  return false;
};

/**
 * Bind the delete module buttons actions.
 */
function bindDeleteModuleAction() {
  // First, bind the delete module handler onto all "delete module" links
  $(".module-options a.delete-module").on("click touchstart", deleteModuleHandler);

  // Then, bind on modal events
  var modal = $("#modal-delete-module");

  // Bind the confirm button on modal
  modal.find("button[data-action='confirm']").on("click", function(event) {
    // Set the "clicked" property to true
    modal.data("submit", "true");
  });

  // Also, bind on modal window open & close
  modal.on("show.bs.modal", function(event) {
    // Remove submit flag
    $(this).removeData("submit");

    // Disable dragging & zooming events on canvas temporarily
    toggleCanvasEvents(false);
  });

  modal.on("hide.bs.modal", function (event) {
    if (_.isEqual($(event.target).data("submit"), "true")) {
      // If modal was successfully submitted, delete the module
      var id = $(event.target).data("module-id");

      deleteModule(id.toString(), true);
    }

    // In any case, re-enable events on canvas
    toggleCanvasEvents(true);
  });
}

function deleteModule(id, linkConnections) {
  var ins = graph.inEdges(id);
  var outs = graph.outEdges(id);
  var tempModuleEl;

  // Remove id from the graph structure, along with all connections
  if (graph.hasNode(id)) {
    graph.removeNode(id);
  }

  // Remove the module <div>, along with all connections
  instance.remove($("#" + id));

  // Connect the sources to destinations
  if (linkConnections) {
    _.each(ins, function(inEdge) {
      _.each(outs, function(outEdge) {
        // Only connect 2 nodes if
        // such a connection doesn't exist already
        if (!graph.hasEdge(inEdge[0], outEdge[1])) {
          graph.addEdge(inEdge[0], outEdge[1]);
          instance.connect({
            source: $("#" + inEdge[0]),
            target: $("#" + outEdge[1])
          });
        }
      });
    });

    //Hide module group options for unconnected modules
    if (outs.length === 0) { // If node is sink
      _.each (ins, function(inEdge) {
        if (graph.degree(inEdge[0]) === 0) {
          tempModuleEl = $("#" + inEdge[0]);
          tempModuleEl.find(".clone-module-group").parents("li").hide();
          tempModuleEl.find(".move-module-group").parents("li").hide();
          tempModuleEl.find(".delete-module-group").parents("li").hide();
        }
      });
    }
    if (ins.length === 0) { // If node is source
      _.each (outs, function(outEdge) {
        if (graph.degree(outEdge[1]) === 0) {
          tempModuleEl = $("#" + outEdge[1]);
          tempModuleEl.find(".clone-module-group").parents("li").hide();
          tempModuleEl.find(".move-module-group").parents("li").hide();
          tempModuleEl.find(".delete-module-group").parents("li").hide();
        }
      });
    }
  }

  // Add ID to the form
  var formAddInput = $('#update-canvas form input#add');
  var formAddNamesInput = $('#update-canvas form input#add-names');
  var formClonedInput = $('#update-canvas form input#cloned');
  var formRemoveInput = $('#update-canvas form input#remove');
  var formMoveInput = $('#update-canvas form input#move');
  var inputVal, newVal;
  var vals, idx;
  var addToRemoveList = true;

  // If the module was moved, we don't need to do anything with it
  inputVal = formMoveInput.attr("value");
  if (!_.isUndefined(inputVal) && inputVal !== "") {
    moved = [];
    $.each(JSON.parse(formMoveInput.val()), function(key, value) {
      if (key.match(/.*,.*/))
        moved = moved.concat(key.split(','));
      else
        moved.push(key);
    });

    if (_.contains(moved, id)) {
      addToRemoveList = false;
      return;
    }
  }

  // If the module we are deleting was added via JS
  // (and hasn't been saved yet), we don't need to "add" it
  // neither "remove" it, it simply ceases to exist
  inputVal = formAddInput.attr("value");
  if (!_.isUndefined(inputVal) && inputVal !== "") {
    vals = inputVal.split(",");
    if (_.contains(vals, id)) {
      addToRemoveList = false;
      idx = vals.indexOf(id);
      vals.splice(idx, 1);
      formAddInput.attr("value", vals.join());
      vals = formAddNamesInput.attr("value").split(SUBMIT_FORM_NAME_SEPARATOR);
      vals.splice(idx, 1);
      formAddNamesInput.attr("value", vals.join(SUBMIT_FORM_NAME_SEPARATOR));
    }
  }

  // Okay, the module was not created, but it might be cloned,
  // so we need to check that as well
  if (!addToRemoveList) {
    inputVal = formClonedInput.attr("value");
    if (!_.isUndefined(inputVal) && inputVal !== "") {
      vals = _.map(inputVal.split(";"), function(val) {
        return val.split(",")[1];
      });
      if (_.contains(vals, id)) {
        addToRemoveList = false;

        // Remove the cloned module from the cloned list
        newVal = "";
        _.each(inputVal.split(";"), function(val) {
          if (!_.isEqual(val.split(",")[1], id)) {
            newVal = (newVal === "" ? "" : (newVal + ";")) + val;
          }
        });
        formClonedInput.attr("value", newVal);
      }
    }
  }

  if (addToRemoveList) {
    inputVal = formRemoveInput.attr("value");
    if (_.isUndefined(inputVal) || inputVal === "") {
      formRemoveInput.attr("value", id);
    } else {
      formRemoveInput.attr("value", inputVal + "," + id);
    }
  }
}

/**
 * Handler function when deleting a single module.
 */
deleteModuleHandler  =function() {
  var id = $(this).data("module-id");
  var modal = $("#modal-delete-module");

  var name = $(".module#" + id).data("module-name");
  var template = modal.find("#message-template").text().trim();

  // Set the modal message
  modal.find("#delete-message").text(template.replace("%{module}", name));

  // Send module id to modal
  modal.data("module-id", id);

  // Display delete modal
  modal.modal({
    "backdrop": "static"
  });

  return false;
};

/**
 * Bind the delete module group buttons actions.
 */
function bindDeleteModuleGroupAction() {
  // First, bind the delete module group handler onto all
  // "delete module group" links
  $(".module-options a.delete-module-group").on("click touchstart", deleteModuleGroupHandler);

  // Then, bind on modal events
  var modal = $("#modal-delete-module-group");

  // Bind the confirm button on modal
  modal.find("button[data-action='confirm']").on("click", function(event) {
    // Set the "clicked" property to true
    modal.data("submit", "true");
  });

  // Also, bind on modal window open & close
  modal.on("show.bs.modal", function(event) {
    // Remove submit flag
    $(this).removeData("submit");

    // Disable dragging & zooming events on canvas temporarily
    toggleCanvasEvents(false);
  });

  modal.on("hide.bs.modal", function (event) {
    if (_.isEqual($(event.target).data("submit"), "true")) {
      // If modal was successfully submitted, delete the module
      var id = $(event.target).data("module-id");

      // Find all modules in the connected component
      var modules = connectedComponents(graph, id.toString());

      // Delete all modules of the module group
      _.each(modules, function(moduleId) {
        deleteModule(moduleId, false);
      });
    }

    // In any case, re-enable events on canvas
    toggleCanvasEvents(true);
  });
}

/**
 * Handler function when deleting module group.
 */
deleteModuleGroupHandler = function() {
  var id = $(this).data("module-id");
  var modal = $("#modal-delete-module-group");

  var name = $(".module#" + id).data("module-name");
  var template = modal.find("#message-template").text().trim();

  // Set the modal message
  modal.find("#delete-message").text(template.replace("%{module}", name));

  // Send module id to modal
  modal.data("module-id", id);

  // Display delete modal
  modal.modal({
    "backdrop": "static"
  });

  return false;
};

/**
 * Bind the clone module action.
 * @param element - jQUery selector for the element on which the click action will run.
 * @param modulesSel - The selector string for all modules.
 * @param gridDistX - The grid distance in X direction.
 * @param gridDistY - The grid distance in Y direction.
 */
function bindCloneModuleAction(element, modulesSel, gridDistX, gridDistY) {
  element.on("click touchstart", function(event) {
    cloneModuleHandler($(this).data("module-id"), modulesSel, gridDistX, gridDistY);
    event.preventDefault();
    event.stopPropagation();
    return false;
  });
}

/**
 * Handler function when cloning a single module.
 * @param moduleId - The ID of the original module.
 * @param modulesSel - The selector string for all modules.
 * @param gridDistX - The grid distance in X direction.
 * @param gridDistY - The grid distance in Y direction.
 */
cloneModuleHandler = function(moduleId, modulesSel, gridDistX, gridDistY) {
  var modules = $(modulesSel);
  var module = modules.filter("#" + moduleId);

  // Position for the cloned module
  var top = elTop(module) + (3 * gridDistY);
  var left = elLeft(module) + (3 * gridDistX);

  cloneModule(module, gridDistX, gridDistY, left, top);

  // Hide all open dropdowns
  $(".module-options").removeClass("open");
  module.css("z-index", 20);
};

/**
 * Clone the original module.
 * @param originalModule - The jQuery original module selector.
 * @param gridDistX - The grid distance in X direction.
 * @param gridDistY - The grid distance in Y direction.
 * @param left - The left position of the new module.
 * @param top - The top position of the new module.
 * @return The new module.
 */
function cloneModule(originalModule, gridDistX, gridDistY, left, top) {
  var moduleId = originalModule.data("module-id");

  // Create new module element
  var id = "n" + newModuleIndex++;
  graph.addNode(id);
  var newModule = createVirtualModule();
  elLeft(newModule, left);
  elTop(newModule, top);
  updateModuleHtml(newModule, id, originalModule.data("module-name"), gridDistX, gridDistY);
  newModule.removeClass("new");

  // Add the cloned module id into the hidden input field
  var formAddInput = $('#update-canvas form input#add');
  var formAddNamesInput = $('#update-canvas form input#add-names');
  var formClonedInput = $('#update-canvas form input#cloned');
  var inputVal, inputNameVal;

  // If we cloned a module with virtual id, there are 2 possibilities:
  // 1. Original module is newly created, which means that our cloned
  // module can also simply be treated as a new module;
  // 2. Original module is a cloned module, which means we want to extend
  // original module's original module into this new module
  var originalId = moduleId;
  var originalWasCloned = false;
  var fillClonedInput = true;

  if (_.isEqual(moduleId.toString().charAt(0), "n")) {
    // Find the original module's "original module", and retrieve its id
    // If such ID cannot be found, original module was not cloned
    fillClonedInput = false;
    inputVal = formClonedInput.attr("value");
    _.each(inputVal.split(";"), function(val) {
      var val2 = val.split(",");
      if (_.isEqual(val2[1], moduleId)) {
        originalId = val2[0];
        fillClonedInput = true;
      }
    });
  }

  if (fillClonedInput) {
    inputVal = formClonedInput.attr("value");
    if (_.isUndefined(inputVal) || inputVal === "") {
      formClonedInput.attr("value", originalId + "," + id);
    } else {
      formClonedInput.attr("value", inputVal + ";" + originalId + "," + id);
    }
  }

  instance.repaintEverything();

  return newModule;
}

/**
 * Bind the clone module group action.
 * @param element - jQUery selector for the element on which the click action will run.
 * @param modulesSel - The selector string for all modules.
 * @param gridDistX - The grid distance in X direction.
 * @param gridDistY - The grid distance in Y direction.
 */
function bindCloneModuleGroupAction(element, modulesSel, gridDistX, gridDistY) {
  element.on("click touchstart", function(event) {
    cloneModuleGroupHandler($(this).data("module-id"), modulesSel, gridDistX, gridDistY);
    event.preventDefault();
    event.stopPropagation();
    return false;
  });
}

/**
 * Handler function when cloning a module group.
 * @param moduleId - The ID of the original module.
 * @param modulesSel - The selector string for all modules.
 * @param gridDistX - The grid distance in X direction.
 * @param gridDistY - The grid distance in Y direction.
 */
cloneModuleGroupHandler = function(moduleId, modulesSel, gridDistX, gridDistY) {
  var modules = $(modulesSel);

  // Retrieve all modules in this module group
  var components = connectedComponents(graph, moduleId.toString());
  var group = _.map(components, function(id) { return $("#" + id); });

  // Calculate the size of the rectangle containing the whole workflow
  var width, height;
  var minX = Number.MAX_VALUE, maxX = -Number.MAX_VALUE;
  var minY = Number.MAX_VALUE, maxY = -Number.MAX_VALUE;

  _.each(group, function(m) {
    var l = elLeft(m);
    var t = elTop(m);
    if (l < minX) { minX = l; }
    if (l > maxX) { maxX = l; }
    if (t < minY) { minY = t; }
    if (t > maxY) { maxY = t; }
  });
  m_width = modules.first().width();
  m_height = modules.first().height();
  width = maxX - minX + m_width + (3 * gridDistX);
  height = maxY - minY + m_height + (3 * gridDistY);

  // Find the appropriate "free space"
  var left = minX != Number.MAX_VALUE ? minX : 0;
  var top = maxY != -Number.MAX_VALUE ? maxY : 0;
  var offset = 0;
  var moduleContained;

  while (true) {
    moduleContained = false;
    for (var i = 0; i < modules.length; i++) {
      var module = $(modules[i]);

      // Skip modules from the module group
      if (_.contains(components, module.data("module-id").toString())) {
        continue;
      }

      var ml = elLeft(module);
      var mt = elTop(module);

      if (ml >= left &&
        ml <= left + width - gridDistX &&
        mt >= top + offset &&
        mt <= top + offset + height - gridDistY) {
        moduleContained = true;
        break;
      }
    }

    // If no module contained, exit
    if (!moduleContained) {
      break;
    }

    offset += gridDistY;
  }

  // Alright, clone all modules from the group and
  // move them by the vertical offset
  clones = {};
  _.each(group, function(m) {
    var nm = cloneModule(m, gridDistX, gridDistY, elLeft(m), elTop(m) + height + offset - gridDistY);

    //Show module group options
    nm.find(".clone-module-group").parents("li").show();
    nm.find(".move-module-group").parents("li").show();
    nm.find(".delete-module-group").parents("li").show();

    clones[m.attr("data-module-id")] = nm.attr("data-module-id");
  });

  // Also, copy the outbound connections
  _.each(_.keys(clones), function(originalId) {
    var clonedId = clones[originalId];

    _.each(graph.successors(originalId), function(outNode) {
      graph.addEdge(clonedId, clones[outNode]);
      instance.connect({
        source: $("#" + clonedId),
        target: $("#" + clones[outNode])
      });
    });
  });

  // Hide all open dropdowns
  $(".module-options").removeClass("open");
  $("#" + moduleId.toString()).css("z-index", 20);

  // Repainting is needed twice (weird, huh)
  instance.repaintEverything();
  instance.repaintEverything();
};

/**
 * Before submission, graph & module group info needs to be
 * copied into hidden input fields via form
 * submission callback.
 * @param gridDistX - The canvas grid distance in X direction.
 * @param gridDistY - The canvas grid distance in Y direction.
 */
function bindEditFormSubmission(gridDistX, gridDistY) {
  $('#update-canvas form').submit(function(){
    var modules = $(".diagram .module");

    updateFormWithModulesData(modules, graph.edges().toString(), gridDistX, gridDistY)

    ignoreUnsavedWorkAlert = true;
    return true;
  });
}

/**
 * Update form with given modules position and connections.
 * @param modules   - Modules, which should be inserted into form
 * @param connections- Connections, which should be inserted into form (empty if
 * no connections)
 * @param gridDistX - The canvas grid distance in X direction.
 * @param gridDistY - The canvas grid distance in Y direction.
 */
function updateFormWithModulesData(modules, connections, gridDistX, gridDistY) {
  var connectionsDiv = $('#update-canvas form input#connections');
  var positionsDiv = $('#update-canvas form input#positions');
  var moduleNamesDiv = $('#update-canvas form input#module-groups');

  // Connections are easy, just copy graph data
  if (connections) {
    if (connectionsDiv.val())
      connectionsDiv.attr("value", connectionsDiv.val() + ',' + connections.toString());
    else
      connectionsDiv.attr("value", connections.toString());
  }

  // Positions are a bit more tricky, but still pretty straightforward
  var moduleGroupNames = {};
  var positionsVal = "";
  var module, id, x, y;
  _.each(modules, function(m) {
    module = $(m);
    id = module.attr("data-module-id");
    x = elLeft(module) / gridDistX;
    y = elTop(module) / gridDistY;
    positionsVal += id + "," + x + "," + y + ";";
    moduleGroupNames[id] = module.attr("data-module-group-name");
  });
  positionsDiv.attr("value", positionsDiv.val() + positionsVal);

  if (moduleNamesDiv.val())
    moduleNamesDiv.attr("value", JSON.stringify($.extend(JSON.parse(moduleNamesDiv.val()),
                                                         moduleGroupNames)));
  else
    moduleNamesDiv.attr("value", JSON.stringify(moduleGroupNames));
}

/**
 * Position the modules onto the canvas.
 * @param modulesSel - The jQuery selector text of module elements.
 * @param gridDistX - The X canvas grid distance.
 * @param gridDistY - The Y canvas grid distance.
 */
function positionModules(modulesSel, gridDistX, gridDistY) {
  var modules = $(modulesSel);

  var module, x, y;
  _.each(modules, function(m) {
    module = $(m);
    x = module.data("module-x");
    y = module.data("module-y");
    elLeft(module, x * gridDistX);
    elTop(module, y * gridDistY);
  });
}

/**
 * Add draggable element/s to the jsPlumb instance.
 * @param elements - The elements selector.
 * @param gridDistX - The grid distance in X direction.
 * @param gridDistY - The grid distance in Y direction.
 */
function addDraggablesToInstance(elements, gridDistX, gridDistY) {
  function handleDragStart(event, ui) {
    var draggedModule = $(event.el);

    leftInitial = elLeft(draggedModule);
    topInitial = elTop(draggedModule);
    collided = false;

    draggedModule
    .css("z-index", "25")
    .addClass("dragged");
  }

  function handleDrag(event, ui) {
    var draggedModule = $(event.el);
    var modules = $(".module");

    // Check if collision occured
    var module;
    for (var i = 0; i < modules.length; i++) {
      module = $(modules[i]);
      if (_.isEqual(module, draggedModule)) {
        continue;
      }

      if (_.isEqual(draggedModule.position(), module.position())) {
        // Collision!
        collided = true;
        break;
      } else {
        collided = false;
      }
    }

    if (collided) {
      draggedModule.addClass("collided");
    } else {
      draggedModule.removeClass("collided");
    }
  }

  function handleDragStop(event, ui) {
    var draggedModule = $(event.el);

    draggedModule
    .css("z-index", "20")
    .removeClass("dragged");

    // Reposition element to back where it was
    if (collided) {
      draggedModule.removeClass("collided");
      elLeft(draggedModule, leftInitial);
      elTop(draggedModule, topInitial);
      instance.repaintEverything();
    }

    collided = false;
  }

  instance.draggable(elements, {
    snapThreshold: Math.max(gridDistX, gridDistY),
    grid: [gridDistX, gridDistY],
    start: handleDragStart,
    drag: handleDrag,
    stop: handleDragStop
  });
}

/**
 * Set the specified elements as drop targets in jsPlumb instance.
 * @param elements - The elements to be used as drop targets.
 */
function setElementsAsDropTargets(elements) {
  instance.makeTarget(elements, {
    dropOptions: { hoverClass: "dragHover" },
    anchor: "AutoDefault",
    allowLoopback: false
  });
}

/**
 * Set the specified elements as drag sources in jsPlumb instance.
 * @param elements - The elements to be used as drag sources.
 * @param anchorStyle - Anchor style.
 * @param connectorStyle - Connector style.
 * @param connectorStyle2 - Connector style 2.
 */
function setElementsAsDragSources(elements, anchorStyle, connectorStyle, connectorStyle2) {
  // CSS_STYLE
  var newAnchorStyle = anchorStyle || DEFAULT_ANCHOR_STYLE;
  var newConnectorStyle = connectorStyle || DEFAULT_CONNECTOR_STYLE;
  var newConnectorStyle2 = connectorStyle2 || DEFAULT_CONNECTOR_STYLE_2;

  instance.makeSource(elements, {
    filter: ".ep",
    anchor: newAnchorStyle,
    connector: newConnectorStyle,
    allowLoopback: false,
    connectorStyle: newConnectorStyle2,
  });
}

/**
 * Zooms the specified instance element, while retaining
 * the non-zoomable children.
 * @param zoomableElement - The element to be zoomed.
 * @param zoom - The zoom level (value between 0 and 1).
 * @param origin - The zoom origin, can be null.
 */
function zoomInstance(zoomableElement, zoom, origin) {
  zoomableElement.css("transform", "scale(" + zoom + ")");

  if (!_.isUndefined(origin)) {
    zoomableElement.css("transform-origin", origin);
  }

  instance.setZoom(zoom);

  // Make sure the no-scale elements are kept on original scale:
  var noscale = zoomableElement.find(".no-scale");
  noscale.css("transform", "scale(" + (1.0 / zoom) + ")");
  noscale.css("transform-origin", "0 0");
}

/**
 * Calculates the draggable element's size.
 * @param draggable - The draggable element.
 * @return - An object {width: <width>, height: <height>}.
 */
function calculateDraggableSize(draggable) {
  // Since draggable's width & height is usually 0,
  // we need to calculate its actual width & height
  // by extracting positions of its children (e.g. modules)
  var minX = Number.MAX_VALUE, maxX = -Number.MAX_VALUE;
  var minY = Number.MAX_VALUE, maxY = -Number.MAX_VALUE;
  var child, left, top, right, bottom;
  _.each(draggable.children(), function(c) {
    child = $(c);
    left = elLeft(child);
    top = elTop(child);
    right = left + child.width();
    bottom = top + child.height();
    if (left < minX) { minX = left; }
    if (right > maxX) { maxX = right; }
    if (top < minY) { minY = top; }
    if (bottom > maxY) { maxY = bottom; }
  });
  if (minX === Number.MAX_VALUE) { minX = -1; }
  if (maxX === -Number.MAX_VALUE) { maxX = 1; }
  if (minY === Number.MAX_VALUE) { minY = -1; }
  if (maxY === -Number.MAX_VALUE) { maxY = 1; }

  return { width: maxX - minX, height: maxY - minY };
}

/**
 * Remembers the draggable element's position.
 * @param draggable - The draggable element.
 * @param parent - The parent element to which the draggable element
 * is relative to.
 */
function rememberDraggablePosition(draggable, parent) {
  // Calculate the center of the draggable's parent X & Y
  var centerX = parent.width() / 2 - elLeft(draggable);
  var centerY = parent.height() / 2 - elTop(draggable);

  var draggableSize = calculateDraggableSize(draggable);

  draggableLeft = centerX / draggableSize.width;
  draggableTop = centerY / draggableSize.height;
}

/**
 * Restores the draggable element's position.
 * @param draggable - The draggable element.
 */
function restoreDraggablePosition(draggable, parent) {
  // Calculate the center of the draggable's parent X & Y
  var centerX = parent.width() / 2;
  var centerY = parent.height() / 2;

  var draggableSize = calculateDraggableSize(draggable);

  var left = draggableLeft * draggableSize.width;
  var top = draggableTop * draggableSize.height;

  elLeft(draggable, centerX - left);
  elTop(draggable, centerY - top);
}

/**
 * Initialize the modules group hover animation.
 * @param modules - The modules jQuery selector.
 * @param sidebar - The sidebar jQuery selector.
 */
function initModulesHover(modules, sidebar) {
  function handlerIn() {
    var groupId = $(this).data("module-group");
    var groupModules = modules.filter("[data-module-group='" + groupId + "']");
    var groupMenu = sidebar.find("li[data-module-group='" + groupId + "']");
    groupModules.addClass("group-hover");
    groupMenu.addClass("group-hover");

    var moduleId = $(this).data("module-id");
    if (!_.isUndefined(moduleId)) {
      var currentModule = modules.filter("[data-module-id='" + moduleId + "']");
      var currentMenu = sidebar.find("li[data-module-id='" + moduleId + "']");
      currentModule.addClass("module-hover");
      currentMenu.addClass("module-hover");
    }
  }
  function handlerOut() {
    var groupId = $(this).data("module-group");
    var groupModules = modules.filter("[data-module-group='" + groupId + "']");
    var groupMenu = $("li[data-module-group='" + groupId + "']");
    groupModules.removeClass("group-hover");
    groupMenu.removeClass("group-hover");

    var moduleId = $(this).data("module-id");
    if (!_.isUndefined(moduleId)) {
      var currentModule = modules.filter("[data-module-id='" + moduleId + "']");
      var currentMenu = sidebar.find("li[data-module-id='" + moduleId + "']");
      currentModule.removeClass("module-hover");
      currentMenu.removeClass("module-hover");
    }
  }
  modules.hover(handlerIn, handlerOut);
  sidebar.find("li[data-module-id]").hover(handlerIn, handlerOut);
  sidebar.find("li[data-module-group]").hover(handlerIn, handlerOut);
}

/**
 * Calculates the specified module group size.
 * @param modules - The modules belonging to this module group.
 * @return - An object {width: <width>, height: <height>}.
 */
function calculateModuleGroupSize(modules) {
  var minX = Number.MAX_VALUE, maxX = -Number.MAX_VALUE;
  var minY = Number.MAX_VALUE, maxY = -Number.MAX_VALUE;
  var module, left, top, right, bottom;
  _.each(modules, function(m) {
    module = $(m);
    left = elLeft(module);
    top = elTop(module);
    right = left + module.width();
    bottom = top + module.height();
    if (left < minX) { minX = left; }
    if (right > maxX) { maxX = right; }
    if (top < minY) { minY = top; }
    if (bottom > maxY) { maxY = bottom; }
  });
  if (minX === Number.MAX_VALUE) { minX = -1; }
  if (maxX === -Number.MAX_VALUE) { maxX = 1; }
  if (minY === Number.MAX_VALUE) { minY = -1; }
  if (maxY === -Number.MAX_VALUE) { maxY = 1; }

  return {
    left: minX,
    top: minY,
    width: maxX - minX,
    height: maxY - minY
  };
}

/**
 * Initialize the modules & groups click action on sidebar
 * so the modules/groups are then centered in canvas.
 * @param modules - The modules jQuery selector.
 * @param sidebar - The sidebar jQuery selector.
 * @param draggable - The canvas draggable jQuery selector.
 * @param parent - The parent of the draggable element.
 * @param modulePadding - The top-left padding form module display.
 */
function initSidebarClicks(modules, sidebar, draggable, parent, modulePadding) {
  function moduleHandler(event) {
    var moduleId = $(this).closest("li").data("module-id");
    var module = modules.filter("[data-module-id='" + moduleId + "']");
    var centerX = parent.width() / 2;
    var centerY = parent.height() / 2;
    var left = centerX - elLeft(module) - (module.width() / 2);
    var top = centerY - elTop(module) - (module.height() / 2);

    event.preventDefault();
    event.stopPropagation();
    animateReposition(draggable, left, top);
    return false;
  }
  function moduleGroupHandler(event) {
    var groupId = $(this).closest("li").data("module-group");
    var groupModules = modules.filter("[data-module-group='" + groupId + "']");
    var groupSize = calculateModuleGroupSize(groupModules);
    var centerX = parent.width() / 2;
    var centerY = parent.height() / 2;
    var left, top;
    if (groupSize.width > parent.width() || groupSize.height > parent.height()) {
      left = -groupSize.left + modulePadding;
      top = -groupSize.top + modulePadding;
    } else {
      left = centerX - groupSize.left - (groupSize.width / 2);
      top = centerY - groupSize.top - (groupSize.height / 2);
    }

    event.preventDefault();
    event.stopPropagation();
    animateReposition(draggable, left, top);
    return false;
  }
  sidebar.find("li[data-module-id] > span > a.canvas-center-on").click(moduleHandler);
  sidebar.find("li[data-module-group] > span > a.canvas-center-on").click(moduleGroupHandler);
}

/**
 * Initialize the jsPlumb by creating a new jsPlumb instance
 * (overrides the currently set global variable 'instance').
 * @param containerSel - The jQuery selector text of the canvas container.
 * @param containerChildSel - The jQuery selector text of the canvas container child.
 * @param modulesSel - The jQuery selector text of module elements.
 * @param params - Various parameters:
 * @param params.gridDistX - Grid distance between modules in X direction.
 * @param params.gridDistY - Grid distance between modules in Y direction.
 * @param params.modulesDraggable - True if modules are draggable.
 * @param params.connectionsEditable - True if connections can be created/edited/removed.
 * @param params.zoomEnabled - True if zooming in/out is enabled.
 * @param params.zoomMin - The minimum zoom level (0. or greater, 1.0 is no zoom).
 * @param params.zoomMax - The maximum zoom level (0. or greater, 1.0 is no zoom).
 * @param params.zoomDist - The distance to travel towards mouse position on zoom.
 * @param params.scrollEnabled - True if dragging/scrolling of content is enabled.
 * @param params.endpointStyle - jsPlumb endpoint style.
 * @param params.connectionHoverStyle - jsPlumb style.
 * @param params.connectionOverlayStyle - jsPlumb style.
 * @param params.connectionLabelStyle - jsPlumb style.
 * @param params.anchorStyle - jsPlumb anchor style.
 * @param params.connectorStyle - jsPlumb endpoint style.
 * @param params.connectorStyle2 - jsPlumb endpoint style.
 */
function initJsPlumb(containerSel, containerChildSel, modulesSel, params) {
  // Functions used for scrolling
  function mouseUp(event) {
    container.off("mousemove touchmove");
  }
  function mouseDown(event) {
    var source = $(event.target);
    if (!$(modulesSel).is(source) &&
        $(modulesSel).has(source).length === 0 &&
        source.is($(container))) {
      // Only do drag & drop if it doesn't
      // origin from module element
      drag_type = DRAG_INVALID;
      if (event.type == "mousedown" && (event.which || event.button) == 1) {
        drag_type = DRAG_MOUSE;
      } else if (event.type == "touchstart" && event.originalEvent.touches.length == 1) {
        drag_type = DRAG_TOUCH;
      }
      if (drag_type > 0) {
        event.preventDefault();
        event.stopPropagation();
        x_start = calcOffsetX(event);
        y_start = calcOffsetY(event);
        container.on("mousemove touchmove", moveDiagram);
      }
    }
    function moveDiagram(event, x_offset, y_offset) {
      // This function is invoked on mousemove
      var x_pos = 0, y_pos = 0, x_el = 0, y_el = 0;
      event.preventDefault();
      event.stopPropagation();
      x_el = draggable.offset().left - draggable.parent().offset().left;
      y_el = draggable.offset().top - draggable.parent().offset().top;
      // Scale offset for X
      // (otherwise, this function only works on scale = 1.0)
      x_so = draggable.parent().width() / 2 * (1 - instance.getZoom());

      var fastOffsetX = calcOffsetX(event);
      var fastOffsetY = calcOffsetY(event);

      x_pos = x_el - x_so + (fastOffsetX - x_start);
      y_pos = y_el + (fastOffsetY - y_start);
      x_start = fastOffsetX;
      y_start = fastOffsetY;
      if (draggable !== null) {
        elLeft(draggable, x_pos);
        elTop(draggable, y_pos);
      }
    }
  }
  // This needs to be here due to Firefox
  function calcOffsetX(e) {
    return (drag_type == DRAG_TOUCH ?
        (e.originalEvent.touches[0].offsetX || e.originalEvent.touches[0].pageX) :
        (e.offsetX || e.pageX)
      ) - $(e.target).offset().left;
  }
  function calcOffsetY(e) {
    return (drag_type == DRAG_TOUCH ?
        (e.originalEvent.touches[0].offsetY || e.originalEvent.touches[0].pageY) :
        (e.offsetY || e.pageY)
      ) - $(e.target).offset().top;
  }

  // Default parameter values
  var params2 = params ? params : {};
  var gridDistX = params2.gridDistX || 350;
  var gridDistY = params2.gridDistY || 350;
  var modulesDraggable = params2.modulesDraggable || false;
  var connectionsEditable = params2.connectionsEditable || false;
  var zoomEnabled = params2.zoomEnabled || false;
  var zoomMin = params2.zoomMin || 0.3;
  var zoomMax = params2.zoomMax || 1.0;
  var zoomDist = params2.zoomDist || 150;
  var scrollEnabled = params2.scrollEnabled || true;

  // CSS_STYLE
  var endpointStyle = params2.endpointStyle || DEFAULT_ENDPOINT_STYLE;
  var connectionHoverStyle = params2.connectionHoverStyle || DEFAULT_CONNECTION_HOVER_STYLE;
  var connectionOverlayStyle = params2.connectionOverlayStyle || DEFAULT_CONNECTION_OVERLAY_STYLE;
  var connectionLabelStyle = params2.connectionLabelStyle || DEFAULT_CONNECTION_LABEL_STYLE;
  var anchorStyle = params2.anchorStyle || null;
  var connectorStyle = params2.connectorStyle || null;
  var connectorStyle2 = params2.connectorStyle2 || null;

  // End of parameters block

  var container = $(containerSel);
  var containerChild = $(containerChildSel);

  // Script for multitouch events
  hammertime = new Hammer(document.getElementById("canvas-container"));
  hammertime.get('pinch').set({ enable: canHammer });

  function hammerZoom (event) {
    zoom = instance.getZoom() * event.scale;
    if (zoom < zoomMin)
      zoom = zoomMin;
    else if (zoom > zoomMax)
      zoom = zoomMax;
    zoomInstance(containerChild, zoom);
  }

  // Setup some styling defaults for jsPlumb
  instance = jsPlumb.getInstance({
    Endpoint: endpointStyle,
    ConnectionOverlays: [ connectionOverlayStyle ],
    Container: containerChild
  });

  window.jsp = instance;
  var modules = $(modulesSel);
  var jsp_modules = jsPlumb.getSelector(modulesSel);
  draggable = $(containerChildSel);

  if (modulesDraggable) {
    // Initialize draggable elements
    addDraggablesToInstance(jsp_modules, gridDistX, gridDistY);
  }

  if (connectionsEditable) {
    instance.bind("beforeDetach", function(connection) {
    });

    // Prevent a connection to be made if it already exists between 2 modules
    instance.bind("beforeDrop", function(info) {
      var newConnection = info.connection;
      var allConnections = instance.getAllConnections();
      var conn;
      for (var i = 0; i < allConnections.length; i++) {
        conn = allConnections[i];
        if (_.isEqual(conn.source, newConnection.source) &&
          _.isEqual(conn.target, newConnection.target)) {
          return false;
        }
      }

      // Now, check if we created a cycle
      var srcNode = $(newConnection.source).attr("data-module-id");
      var targetNode = $(newConnection.target).attr("data-module-id");
      graph.addEdge(srcNode, targetNode);
      if (!jsnx.isDirectedAcyclicGraph(graph)) {
        graph.removeEdge(srcNode, targetNode);
        return false;
      }

      var srcModuleEl = $("#" + srcNode);
      var targetModuleEl = $("#" + targetNode);

      //Modules should belong to module group now
      //Show module group options for target and source

      srcModuleEl.find(".clone-module-group").parents("li").show();
      srcModuleEl.find(".move-module-group").parents("li").show();
      srcModuleEl.find(".delete-module-group").parents("li").show();

      targetModuleEl.find(".clone-module-group").parents("li").show();
      targetModuleEl.find(".move-module-group").parents("li").show();
      targetModuleEl.find(".delete-module-group").parents("li").show();
      return true;
    });

    // Bind a connection listener. Note that the parameter passed to this function contains more than
    // just the new connection - see the documentation for a full list of what is included in 'info'.
    // this listener sets the connection's internal
    // id as the label overlay's text.
    instance.bind("connection", function (info) {
    _.each(instance.getAllConnections(), function(conn) {
      conn.endpoints[0].setEnabled(false);
      conn.endpoints[1].setEnabled(false);
      });
    });

    // Bind a click listener to each connection
    instance.bind("click", function (c) {
      // Remove the edge from our graph data structure
      graph.removeEdge(c.sourceId, c.targetId);

      // Remove edge from GUI
      instance.detach(c);

      // Hide module group options if source or target module
      // is not part of a module group anymore

      var srcModuleEl = $("#" + c.sourceId);
      var targetModuleEl = $("#" + c.targetId);
      //First source
      if (graph.degree(c.sourceId) === 0) {
        srcModuleEl.find(".clone-module-group").parents("li").hide();
        srcModuleEl.find(".move-module-group").parents("li").hide();
        srcModuleEl.find(".delete-module-group").parents("li").hide();
      }
      if (graph.degree(c.targetId) === 0) {
        targetModuleEl.find(".clone-module-group").parents("li").hide();
        targetModuleEl.find(".move-module-group").parents("li").hide();
        targetModuleEl.find(".delete-module-group").parents("li").hide();
      }
    });
  }

  // Suspend drawing and initialize
  if (modules.length > 0) {
    instance.batch(function () {
      // Set elements as connection sources
      setElementsAsDragSources(jsp_modules, anchorStyle, connectorStyle, connectorStyle2);

      // Initialise all elements as connection targets
      setElementsAsDropTargets(jsp_modules);

      // Initialize module connections
      var module, outs;
      _.each(modules, function(m) {
        module = $(m);
        outs = graph.successors(module.attr("data-module-id"));
        _.each(outs, function(out) {
          instance.connect({source: module, target: $("div[data-module-id=" + out + "]")})
        });
      });
    });
  }

  // Enable/disable connection endpoints
  /*
  _.each(instance.getAllConnections(), function(conn) {
    conn.endpoints[0].setEnabled(connectionsEditable);
    conn.endpoints[1].setEnabled(connectionsEditable);
  });
  */
  _.each(instance.getAllConnections(), function(conn) {
    conn.endpoints[0].setEnabled(false);
    conn.endpoints[1].setEnabled(false);
  });
  // Update style on existing connections
  if (connectionsEditable) {
    _.each(instance.getAllConnections(), function(conn) {
      conn.setHoverPaintStyle(connectionHoverStyle);
      conn.setLabel(connectionLabelStyle);
    });
  }

  // Make sure the new connections will have same style
  if (connectionsEditable) {
    instance.importDefaults({
      HoverPaintStyle: connectionHoverStyle,
      ConnectionOverlays: [
        connectionOverlayStyle,
        [ "Label", connectionLabelStyle ]
      ]
    });
  } else {
    instance.importDefaults({
      HoverPaintStyle: connectionHoverStyle,
      ConnectionOverlays: [ connectionOverlayStyle ]
    });
  }

  // Enable / disable instance configs
  if (modules.length > 0) {
    instance.setDraggable(jsp_modules, modulesDraggable);
    instance.setSourceEnabled(jsp_modules, connectionsEditable);
    instance.setTargetEnabled(jsp_modules, connectionsEditable);
  }

  // Bind the mouse scroll event to scaling
  if (zoomEnabled) {
    container.mousewheel(function(event) {
      zoom = instance.getZoom() + (event.deltaY / 20);
      if (zoom < zoomMin || zoom > zoomMax) {
        return;
      }
      zoomInstance(containerChild, zoom);
    });

    hammertime.on('pinch', hammerZoom);
  }

  if (scrollEnabled) {
    // Make the jsPlumb container movable/draggable for "google maps" effect
    container.on("mousedown touchstart", mouseDown);
    container.on("mouseup mouseout touchend", mouseUp);
  }

  // If this is not triggered, dropdown in edit mode
  // don't work on Firefox prior to zooming the canvas
  zoomInstance(containerChild, 1.0);

  jsPlumb.fire("jsPlumbLoaded", instance);
}

// Opens edit mode if redirected from empty experiment
(function noWorkflowimgEditMode(){
  if( getParam('editMode') ){
    $("#edit-canvas-button").click();
  }
})();

/** prevent reload page */
var preventCanvasReloadOnSave = (function() {
  'use strict';

  function confirmReload() {
    if( confirm(I18n.t('experiments.canvas.reload_on_submit')) ) {
      return true;
    } else {
      return false
    }
  }

  function preventCanvasReload() {
    document.onkeydown = function(){
      switch (event.keyCode){
        case 116:
          event.returnValue = false;
          return confirmReload();
        case 82:
          if (event.ctrlKey){
            event.returnValue = false;
            return confirmReload();
          }
        }
    }
  }

  function bindToCanvasSave(fun) {
    $('#canvas-save').on('click', function() {
      fun();
    })
  }

  return function() { bindToCanvasSave(preventCanvasReload) };
})();