/**
 * sortable.js
 * -----------
 *
 * This file is included on any pages that have drag & drop lists. Some lingo:
 *   "sortable":        the actual sortable lists.
 *   "groupable":       sortables that contain rows may be joined / separated for dragging
 *                      as a single unit
 *   "sortable groups": where there are more than one distinct sortable lists and the user
 *                      can drag items from one group to the other.
 *
 * This file contains code for all scenarios:
 *    - sortables
 *    - sortables + groupable items
 *    - grouped sortables
 *    - grouped sortables with groupable items
 *
 * It works by automatically parsing the DOM, assigning the various event handlers and initializing
 * the sortables.
 */

$(function () {

	// initialization code UNIQUE to sortable groups
	$(".sortable_groups").each(function () {
		var id = $(this).attr("id");

		var custom_delete_handler = null;
		var found = $("#" + id).find(".sortable__custom_delete_handler");
		if (found.length > 0) {
			var parts = found.val().split(".");
			custom_delete_handler = window[parts[0]][parts[1]];
		}
		var custom_delete_group_handler = null;
		var found = $("#" + id).find(".sortable__delete_group_handler");
		if (found.length > 0) {
			var parts = found.val().split(".");
			custom_delete_group_handler = window[parts[0]][parts[1]];
		}
		sortable_ns.page_sortables[id] = {
			type: "sortable_group",
			custom_delete_handler: custom_delete_handler,
			custom_delete_group_handler: custom_delete_group_handler,
			delete_tooltip: $("#" + id).find(".sortable__delete_tooltip").val(),
			edit_tooltip: $("#" + id).find(".sortable__edit_tooltip").val(),
			deleted_rows: []
		};

		$(this).closest("form").bind("submit", function () {
			sortable_ns.sortable_submit(this);
		});
		sortable_ns._add_common_event_delegation(this);

		// TODO needs sortupdate to reset tabindexes..,
		$(this).sortable({
			axis: "y",
			handle: ".sort"
		});
	});


	// initialization code UNIQUE to non-grouped sortables
	$(".sortable").each(function () {

		// if this sortable is part of a group, ignore it! This stuff is all taken care of already
		if ($(this).closest(".sortable_groups").length > 0) {
			return;
		}
		var id = $(this).attr("id");

		var custom_delete_handler = null;
		var found = $("#" + id).find(".sortable__custom_delete_handler");
		if (found.length > 0) {
			var parts = found.val().split(".");
			custom_delete_handler = window[parts[0]][parts[1]];
		}
		sortable_ns.page_sortables[id] = {
			type: "sortable",
			custom_delete_handler: custom_delete_handler,
			custom_delete_group_handler: null,
			delete_tooltip: $("#" + id).find(".sortable__delete_tooltip").val(),
			edit_tooltip: $("#" + id).find(".sortable__edit_tooltip").val(),
			deleted_rows: []
		};

		$(this).closest("form").bind("submit", function () {
			sortable_ns.sortable_submit(this);
		});
		sortable_ns._add_common_event_delegation(this);
	});

	// fine. Should always be run, regardless of whether they are sortable groups or a simple sortable
	$(".groupable").each(function () {
		$(this).find(".sortable_row").prepend("<div class=\"group_block_top\"> </div>");
		$(this).find(".row_group>ul").prepend("<li class=\"col0\"></li>");
	});

	$(".sortable .sortable_row .sort_col").live("mouseover", function (e) {
		if (!$(e.target).hasClass("col0")) {
			$(this).closest(".row_content").addClass("over");
		}
	}).live("mouseout", function (e) {
		$(this).closest(".row_content").removeClass("over");
	});

	// alright! Now we do stuff for ALL sortable elements, regardless of whether they're part of a grouped sortable or not
	$(".sortable").each(function (e) {
		$(this).find(".sortable_row").last().addClass("rowN");
		$(this).find(".sortable_row .row_group").last().addClass("rowN");
		$(this).find(".rows").each(function () {
			var curr_sortable_rows = this;

			// ONLOAD, ADD ROW(S), SORTUPDATE FOR ROWS, SORTUPDATE FOR GROUP
			sortable_ns.update_tab_indexes(curr_sortable_rows, true);

			// ONLOAD, AFTER NEW GROUP INSERT
			$(curr_sortable_rows).sortable({
				axis: "y",
				connectWith: ".connected_sortable",
				handle: ".sort_col"
			});

			$(curr_sortable_rows).bind("sortupdate", function (e, ui) {
				sortable_ns.reorder_rows(curr_sortable_rows);
			});
		});
	});


	$(".add_group_link").live("click", sortable_ns.add_group);

	// used for rare cases
	if ($(".sortable__row_offset").length == 1) {
		sortable_ns.row_offset = parseInt($(".sortable__row_offset").val()) - 1;
	}
});


var sortable_ns = {
	// initialized on page load. It's a hash of TOP sortable element IDs as the keys, to info about the sortable
	// (custom event handlers, deleted rows, etc).
	page_sortables: {},

	// shared stuff
	connect_rows_icon: "group_block_connect.png",
	disconnect_rows_icon: "group_block_disconnect.png",
	delete_group_dialog: $("<div id=\"delete_group_dialog\"></div>"),
	delete_nonempty_group_dialog: $("<div id=\"delete_nonempty_dialog\"></div>"),

	// this is only used for the Edit Fields page, when you have a sortable that starts on a different row (e.g. page 2).
	// This should really be moved to be stored in page_sortables, but it's just not important right now
	row_offset: 0
};


/**
 * This is used on both sortables and grouped sortables. It adds a number of event delegated
 * functionality to the TOP element:
 *     - mouseover and mouseout rows: edit / delete, join/separate rows icon
 *     - clicks events: deleting rows, joining/separating rows
 *     - deleting group (only needed for grouped sortables, of course, but it doesn't add much weight
 *       when not needed)
 */
sortable_ns._add_common_event_delegation = function (el) {
	var sortable_id = $(el).attr("id");

	$(el).bind("mouseover", function (e) {
		if ($(e.target).hasClass("del")) {
			$(e.target).closest(".row_group").addClass("delete_row_hover");
		} else if ($(e.target).hasClass("edit") || $(e.target).parent().hasClass("edit")) {
			$(e.target).closest(".row_group").addClass("edit_row_hover");
		} else if ($(e.target).hasClass("col0")) {
			var curr_col0 = e.target;
			var has_next_row_group_in_sortable_row = ($(curr_col0).closest(".row_group").next().length > 0);
			if (!$(curr_col0).closest(".sortable_row").next().length && !has_next_row_group_in_sortable_row) {
				return;
			}
			var action = (has_next_row_group_in_sortable_row) ? "disconnect" : "connect";
			sortable_ns._update_connect_image({ el: curr_col0, action: action });
		}
	});

	$(el).bind("mouseout", function (e) {
		if ($(e.target).hasClass("del")) {
			$(e.target).closest(".row_group").removeClass("delete_row_hover");
		} else if ($(e.target).hasClass("edit") || $(e.target).parent().hasClass("edit")) {
			$(e.target).closest(".row_group").removeClass("edit_row_hover");
		} else if ($(e.target).hasClass("col0")) {
			$(e.target).css({
				"background": "",
				"cursor": "auto"
			});
			sortable_ns.__current_sortable_connection_chain = null;
		}
	});

	$(el).bind("click", function (e) {

		// the default delete behaviour is just to fade out the row: the row will actually be deleted on the server
		// after the user updates the form. However, it can be overridden to do Ajax deletes or pop open a confirmation
		// window or whatever is needed. For that, you'll need a custom_delete_handler
		if ($(e.target).hasClass("del")) {
			if (sortable_ns.page_sortables[sortable_id].custom_delete_handler != null) {
				sortable_ns.page_sortables[sortable_id].custom_delete_handler(e.target);
			} else {
				sortable_ns.delete_row(sortable_id, e.target);
			}
		}

		// get all row_groups in the NEXT sortable row, append them to the current sortable_row, then
		// delete the next one
		else if ($(e.target).hasClass("col0")) {
			if (sortable_ns.__current_sortable_connection_chain == "connect") {
				var curr_sortable_row = $(e.target).closest(".sortable_row");
				curr_sortable_row.find(".row_content").addClass("grouped_row");
				var next_sortable_row = curr_sortable_row.next();
				var next_row_groups = next_sortable_row.find(".row_content").children();

				// N.B. this doesn't clone the node: it MOVES it. This is necessary to prevent having
				// to update the scroll-bar (if defined) which references the actual DOM node
				curr_sortable_row.find(".row_group:last").after(next_row_groups);
				next_sortable_row.remove();
				sortable_ns.__current_sortable_connection_chain = "disconnect";
				sortable_ns._update_connect_image({ el: e.target, action: "disconnect" });
			} else if (sortable_ns.__current_sortable_connection_chain == "disconnect") {
				sortable_ns._disconnect_row($(e.target).closest(".row_group"));
				sortable_ns.__current_sortable_connection_chain = "connect";
				sortable_ns._update_connect_image({ el: e.target, action: "connect" });
			}

			// any time rows are joined / separates, reorder the parent sortable
			sortable_ns.reorder_rows($(el).closest(".sortable"));
		}

		// if there's a custom delete group handler specified, it passes off all work to that function. Otherwise,
		// it opens a confirmation dialog that (when agreed to) simply submits the parent form with a
		// [sortable ID prefix]_sortable__delete_group hidden field with the appropriate value - the server-side
		// code can figure out what to delete, depending on the data set.
		else if ($(e.target).hasClass("delete_group")) {
			if (sortable_ns.page_sortables[sortable_id].custom_delete_group_handler != null) {
				sortable_ns.page_sortables[sortable_id].custom_delete_group_handler(e.target);
			} else {
				ft.create_dialog({
					dialog: sortable_ns.delete_group_dialog,
					title: g.messages["phrase_please_confirm"],
					content: g.messages["confirm_delete_group"],
					popup_type: "warning",
					buttons: [{
						"text": g.messages["word_yes"],
						"click": function () {
							ft.dialog_activity_icon(sortable_ns.delete_group_dialog, "show");
							var group_id = $(e.target).closest(".sortable_group_header").find(".group_order").val();
							var form = $(e.target).closest("form");

							form.append("<input type=\"hidden\" name=\"" + sortable_id + "_sortable__delete_group\" class=\"sortable__delete_group\" value=\"" + group_id + "\" />");
							form.trigger("submit");
						}
					},
						{
							"text": g.messages["word_no"],
							"click": function () {
								$(this).dialog("close");
							}
						}]
				});
			}
			return false;
		}
		;
	});
}


// called after the user re-orders or deletes a row
sortable_ns.reorder_rows = function (sortable_el, update_tab_indexes) {

	// hide/show the default empty row. The empty row is required, to get around a small bug with the
	// jQuery sortable. If it's not there, items can't be dragged to the empty group
	var num_sortable_rows = $(sortable_el).find(".sortable_row").length;
	if (num_sortable_rows > 1) {
		$(sortable_el).find(".empty_group").hide();
	} else {
		$(sortable_el).find(".empty_group").show();
	}

	var counter = 1 + sortable_ns.row_offset;
	$(sortable_el).find(".sort_col").each(function () {
		$(this).html(counter);
		counter++;
	});

	$(sortable_el).find(".sortable_row").removeClass("rowN").last().addClass("rowN");
	$(sortable_el).find(".sortable_row .row_group").removeClass("rowN").not(".empty_group").last().addClass("rowN");

	// finally, re-assign all the tabindexes [move this to an event-delegated focus]
	if (update_tab_indexes === true) {
		sortable_ns.update_tab_indexes(sortable_el);
	}

	// sometimes when dropping a selected row, the hover class is orphaned. This explicitly removes it
	$(sortable_el).find(".row_content").removeClass("over");
}


sortable_ns.update_tab_indexes = function (sortable_el) {
	var selectors = $(sortable_el).closest(".sortable").find(".tabindex_col_selectors");
	if (selectors.length) {
		selectors = selectors.val().split("|");
		var count = 1; // ...! TODO ... dude!
		for (var i = 0; i < selectors.length; i++) {
			$(selectors[i]).each(function () {
				$(this).attr("tabindex", count++);
			});
		}
	}
}


// used in sortable tables. This returns the necessary markup to append to .rows.
sortable_ns.get_sortable_row_markup = function (info) {
	var settings = $.extend({
		row_group: info.row_group,
		is_grouped: true
	}, info);

	var li = $("<li class=\"sortable_row\"></li>");
	if (settings.is_grouped) {
		li.append("<div class=\"group_block_top\"></div>");
	}

	var grouped_row_class = "";
	if (settings.is_grouped) {
		var group_count = $("<div></div>").append(settings.row_group).find(".row_group").length;
		grouped_row_class = (group_count > 1) ? " grouped_row" : "";
	}

	var row_content = $("<div class=\"row_content" + grouped_row_class + "\"></div>");
	row_content.append(settings.row_group);
	li.append(row_content);
	li.append("<div class=\"clear\"></div>");

	return li;
}


/**
 * Automatically called when a form containing a sortable is submitted. It appends three hidden fields to
 * the form. [prefix] is the unique sortable ID of the top level sortable element.
 *
 *     [prefix]sortable__rows: a comma delimited list of rows. If the form contains grouped sortables,
 *                             this contains a string of the form:
 *                             group_id1|sortable_id1,sortable_id2,...`group_id2|sortable_id1...
 *
 *     [prefix]sortable__new_groups: a comma delimited list of row numbers that are the start of a
 *                             new sort group.
 *
 *     [prefix]sortable__deleted_rows: a comma delimited list of rows that were deleted. In order for
 *                             this to be useful, the row numbers have to map to something on the server,
 *                             like field IDs or View IDs. If the rows were just temporarily
 *                             created in the client then deleted, this contains no value.
 */
sortable_ns.sortable_submit = function (form_el) {

	// remove any sortable submit info already appended to the page in case this was already called but cancelled
	$("input[name$=sortable__rows],input[name$=sortable__new_groups],input[name$=sortable__deleted_rows]").remove();

	for (var sortable_id in sortable_ns.page_sortables) {
		var sortable_info = sortable_ns.page_sortables[sortable_id];

		// technically, we could have separate sortables in multiple forms on the page (even though we don't do this yet)
		// so just in case, check that this current sortable is part of the form being submitted. If not, ignore the sucker
		if ($(form_el).find("#" + sortable_id).length == 0) {
			alert("This sortable is not in the form. Please report this problem.");
			continue;
		}

		var row_order_list = [];
		var sortable__rows = "";

		if (sortable_info.type == "sortable_group") {
			$("#" + sortable_id).find(".sortable_group").each(function () {
				var group_str = $(this).find(".sortable_group_header .group_order").val();
				var row_ids = [];
				$(this).find(".sr_order").each(function () {
					row_ids.push($(this).val());
				});
				group_str += "|" + row_ids.toString();
				row_order_list.push(group_str);
			});
			sortable__rows = row_order_list.join("~");
		} else {
			$("#" + sortable_id).find(".sr_order").each(function () {
				row_order_list.push($(this).val());
			});
			sortable__rows = row_order_list.toString();
		}

		var new_groups = [];
		$("#" + sortable_id).find(".sortable_row").not(".empty_group").each(function () {
			new_groups.push($(this).find(".row_group:first input.sr_order").val());
		});

		$(form_el).append("<input type=\"hidden\" name=\"" + sortable_id + "_sortable__rows\" value=\"" + sortable__rows + "\" />"
			+ "<input type=\"hidden\" name=\"" + sortable_id + "_sortable__new_groups\" value=\"" + new_groups.toString() + "\" />"
			+ "<input type=\"hidden\" name=\"" + sortable_id + "_sortable__deleted_rows\" value=\"" + sortable_info.deleted_rows + "\" />");
	}
}


sortable_ns.delete_row = function (sortable_id, el) {
	var sortable_row = $(el).closest(".sortable_row");
	var num_row_groups = $(el).closest(".row_content").find(".row_group").length;
	var curr_row_group = $(el).closest(".row_group");
	var row_number = curr_row_group.find(".sr_order").val();

	// delete the row_group. This is the div containing the row <ul> and the following clearer div
	curr_row_group.fadeOut(200);

	// log the deleted row numbers
	sortable_ns.page_sortables[sortable_id].deleted_rows.push(row_number);

	// if the parent .row_content div doesn't contain any more .row_group divs, delete the entire sortable row.
	// Otherwise just delete the specific .row_group
	setTimeout(function () {
		var sortable_el = $(el).closest(".sortable");
		if (num_row_groups == 1) {
			sortable_row.remove();
		} else {
			curr_row_group.remove();
		}
		sortable_ns.reorder_rows(sortable_el, false);
	}, 220);
}


sortable_ns._update_connect_image = function (info) {
	var image = (info.action == "connect") ? sortable_ns.connect_rows_icon : sortable_ns.disconnect_rows_icon;
	var tooltip = (info.action == "connect") ? g.messages["phrase_connect_rows"] : g.messages["phrase_disconnect_rows"];
	sortable_ns.__current_sortable_connection_chain = info.action;

	$(info.el).attr("title", tooltip);
	$(info.el).css({
		"cursor": "pointer",
		"background": "url(" + g.root_url + "/global/images/" + image + ")"
	});
}


/**
 * Helper function to actually disconnect a row in a groupable-sortable.
 */
sortable_ns._disconnect_row = function (row_group) {
	// if there is only one item before this one being disconnected, remove the grouped_row class
	var previous_row_groups = row_group.prevUntil();
	if (previous_row_groups.length < 1) {
		$(row_group).closest(".row_content").removeClass("grouped_row");
	}

	// get all subsequent row_groups and move them into a new sortable_row
	var following_row_groups = row_group.nextUntil();
	var grouped_row_class = (following_row_groups.length > 1) ? " grouped_row" : "";

	var new_li = $("<li class=\"sortable_row\"><div class=\"group_block_top\"></div></li>");
	var row_content = $("<div class=\"row_content" + grouped_row_class + "\"></div>");
	row_content.append(following_row_groups);
	new_li.append(row_content);
	new_li.append("<div class=\"clear\"></div>");
	$(row_group).closest(".sortable_row").after(new_li);
}


sortable_ns._has_previous_field = function (row_group) {
	if (row_group.prev().length > 0) {
		return true;
	} else {
		return $(row_group).closest(".sortable_row").prev().length > 0;
	}
}


sortable_ns._has_next_field = function (row_group) {
	if (row_group.next().length > 0) {
		return true;
	} else {
		return $(row_group).closest(".sortable_row").next().length > 0;
	}
}

sortable_ns._get_previous_row_group = function (row_group) {
	var next_row_group = null;
	if (row_group.prev().length > 0) {
		next_row_group = row_group.prev();
	} else {
		if ($(row_group).closest(".sortable_row").prev().length > 0) {
			next_row_group = $(row_group).closest(".sortable_row").prev().find(".row_group:last");
		}
	}
	return next_row_group;
}


sortable_ns._get_next_row_group = function (row_group) {
	var next_row_group = null;
	if (row_group.next().length > 0) {
		next_row_group = row_group.next();
	} else {
		if ($(row_group).closest(".sortable_row").next().length > 0) {
			next_row_group = $(row_group).closest(".sortable_row").next().find(".row_group:first");
		}
	}
	return next_row_group;
}


/**
 * This function is called whenever the user clicks on an "Create New Group >>" link. All it
 * does is create a new dialog where they enter the group name, then farm out the actual work
 * to a custom function. This lets us re-use this functionality anywhere and execute any
 * custom code we need. Generally, the calling function will do an Ajax request to create the
 * new group, then call sortable_ns.insert_new_group to insert it in the page - all without a
 * server reload.
 *
 * If this function is too restrictive (e.g. you need to change the width or some other attribute
 * of the popup, don't give the .
 *
 * e.g. on the Edit View Fields tab.
 */
sortable_ns.add_group = function () {
	// assumes the add group function is object namespaced, like: my_ns.my_function
	var parts = $(".sortable__add_group_handler")[0].value.split(".");
	var add_group_handler = window[parts[0]][parts[1]];

	ft.create_dialog({
		dialog: $("#add_group_popup"),
		title: $(".add_group_popup_title").val(),
		buttons: [{
			"text": g.messages["phrase_create_group"],
			"click": function () {
				add_group_handler();
			}
		},
			{
				"text": g.messages["word_cancel"],
				"click": function () {
					$(this).dialog("close");
				}
			}]
	});

	return false;
}


/**
 * Dynamically inserts a new group into the page, at the end of .sortable_groups. This function is
 * strongly coupled with the page, because unfortunately the markup is pretty custom for each use -
 * classes, labels, IDs etc.
 *
 * Most groups just have: label - input field for group name - delete icon. To use that, you need the following:
 *   - hidden field in page with class "sortable__new_group_name" containing the
 *   - hidden field in page with class "sortable__class" that contains the main class name for the group
 *
 * If you need additional customization, just don't call this function: define your own and use
 * sortable_ns.append_new_sortable_group() to actually do the job of inserting the new group.
 *
 * @param object info - MUST contain group_name and group_id params
 */
sortable_ns.insert_new_group = function (info) {

	var info = $.extend({
		// required!
		group_name: "",
		group_id: "",

		// optional
		is_groupable: false
	}, info);

	var group_label = $(".sortable__new_group_name").val();
	var sortable_class = $(".sortable__class").val();

	var html = "<div class=\"sortable_group\">\n"
		+ "<div class=\"sortable_group_header\">\n"
		+ "<div class=\"sort\"></div>\n"
		+ "<label>" + group_label + "</label>\n"
		+ "<input type=\"text\" name=\"group_name_" + info.group_id + "\" class=\"group_name\" value=\""
		+ info.group_name.replace(/"/, "&quot;") + "\" />\n"
		+ "<div class=\"delete_group\"></div>\n"
		+ "<input type=\"hidden\" class=\"group_order\" value=\"" + info.group_id + "\" />\n"
		+ "<div class=\"clear\"></div>\n"
		+ "</div>\n"
		+ "<div class=\"sortable " + sortable_class + "\">\n";

	html += $("#sortable__new_group_header").html();

	html += "<div class=\"clear\"></div>\n"
		+ "<ul class=\"rows connected_sortable";

	if (info.is_groupable) {
		html += " groupable";
	}

	html += "\">\n"
		+ "<li class=\"sortable_row rowN empty_group\"><div class=\"clear\"></div></li>"
		+ "</ul>\n"
		+ "</div>\n"
		+ "<div class=\"clear\"></div>\n";

	if ($("#sortable__new_group_footer").length) {
		html += $("#sortable__new_group_footer").html();
	}

	html += "</div>\n";

	sortable_ns.append_new_sortable_group(html);
}


/**
 * Helper function to actually append the new sortable group to the end of the sortable groups. It
 * also does a little stuff behind the scenes to ensure that the new sortable group is tied to
 * the existing sortables, etc.
 *
 * @param string html
 */
sortable_ns.append_new_sortable_group = function (html) {
	$(".sortable_groups").append(html);

	// now re-create the sortables
	$(".sortable_groups").find(".rows").each(function () {
		// destroy the old sortable
		$(this).sortable("destroy");
		$(this).unbind("sortupdate");

		var curr_sortable_rows = this;
		$(this).sortable({
			axis: "y",
			connectWith: ".connected_sortable",
			handle: ".sort_col"
		});

		$(this).bind("sortupdate", function (e, ui) {
			sortable_ns.reorder_rows(this);
		});
	});
}
