/**
 * Mylist Page
 * @requires jQuery
 * @requires jQuery.history
 * @requires Prototype
 * @requires Jarty
 * @requires NicoAPI
 * @copyright 2009 DWANGO Co., LTD. All rights reserved.
 */

(function ($) {

/* 設定 */

var config = {
	items_per_page: 100,                    // 1ページあたりのアイテム数.
	new_mylistgroup_name: "新しいマイリスト(%d)",   // マイリストグループを新規作成する時の名前.
	first_mylistgroup_description:          // 初めてマイリストグループを作った時のメモ.
		"このスペースはメモ欄としてご自由にお使い下さい！",
	title_matcher: /^.*(‐[^‐]+)$/,          // タイトルを置き換えるための正規表現.
	message_stay_time: 3000                 // メッセージを表示する時間 (msec)
};

var messages = {
	confirm_remove_mylistgroup: "本当にこのマイリストを削除してもよろしいですか？\n\n※この操作は取り消せません！",
	confirm_remove_mylist: "本当に削除してもよろしいですか？",
	copy_success: Jarty.compile("{$total} 件をコピーしました"),
	copy_duplicates: Jarty.compile("{if $total > $dup}{$total} 件のうち {$dup} 件は{/if}すでにコピー先に存在しているためコピーできませんでした"),
	move_success: Jarty.compile("{$total} 件を移動しました"),
	move_duplicates: Jarty.compile("{if $total > $dup}{$total} 件のうち {$dup} 件は{/if}すでに移動先に存在しているため移動できませんでした"),
	remove_success: Jarty.compile("{$total} 件を削除しました"),
	nonexist_mylistgroup: "このマイリストは削除されています",
	maintenance: "メンテナンス中により利用できません",
	api_failure: {
		"NOAUTH": "ログインしていません\nお手数ですがページを更新してください",
		"INVALIDTOKEN": "別のユーザーでログインしています\nお手数ですがページを更新してください",
		"EXPIRETOKEN": "ページの有効期限が過ぎました\nお手数ですがページを更新してください",
		"INTERNALERROR": "処理の実行に失敗しました\nお手数ですがページを更新してください",
		"NONEXIST": "存在しないデータに対して処理を行いました\nお手数ですがページを更新してください",
		"EXIST": "すでに登録されています",
		"MAXERROR": "上限に達しているため処理を実行できませんでした",
		"UNKNOWN": "通信中にエラーが発生しました\nお手数ですがページを更新してください"
	}
};

// アイテム種別 (ID => 種別名)
var itemtypes = {
	0: "video",
	5: "seiga"
};

// マイリストアイテムのソート順 (ID => 比較関数)
var itemsorts = {
	0:  item_comparer(["create_time"], false, true),
	1:  item_comparer(["create_time"], true, true),
	2:  item_comparer(["description"], false),
	3:  item_comparer(["description"], true),
	4:  item_comparer(["title"], false),
	5:  item_comparer(["title"], true),
	6:  item_comparer(["item_data.first_retrieve", "item_data.create_time"], true, true),
	7:  item_comparer(["item_data.first_retrieve", "item_data.create_time"], false, true),
	8:  item_comparer(["item_data.view_counter", "item_data.view_count"], true, true),
	9:  item_comparer(["item_data.view_counter", "item_data.view_count"], false, true),
	10: item_comparer(["item_data.update_time"], true, true),
	11: item_comparer(["item_data.update_time"], false, true),
	12: item_comparer(["item_data.num_res", "item_data.comment_count"], true, true),
	13: item_comparer(["item_data.num_res", "item_data.comment_count"], false, true),
	14: item_comparer(["item_data.mylist_counter", "item_data.mylist_count"], true, true),
	15: item_comparer(["item_data.mylist_counter", "item_data.mylist_count"], false, true),
	16: item_comparer(["item_data.length_seconds"], true, true),
	17: item_comparer(["item_data.length_seconds"], false, true),
	18: item_comparer(["item_data.res_count"], true, true),
	19: item_comparer(["item_data.res_count"], false, true)
};

// マイリストグループのソート順 (ID => 比較関数)
var groupsorts = {
	0:  group_comparer("create_time", false),
	1:  group_comparer("create_time", true),
	2:  group_comparer("name", true),
	3:  group_comparer("name", false),
	4:  group_comparer("description", true),
	5:  group_comparer("description", false)
};

/* グローバル変数 */

var my = {
	groups: undefined,              // マイリストグループの配列
	group: {},                      // グループIDをキー, マイリストグループを値とする辞書
	deflist: undefined,             // デフォルトマイリストのアイテム配列
	mylist: {},                     // グループIDをキー, マイリストのアイテム配列を値とする辞書
	mymemory: undefined,            // マイメモリーのアイテム配列
	currentPage: undefined,         // 現在のページ
	currentGroup: undefined,        // 現在選択されているマイリストグループ
	currentItems: undefined         // 現在選択されているグループのアイテム配列
};

Jarty.globals({ config: config, my: my });

/* グローバルイベント */

$.each([
	"nicoPageChanged",              // 表示しているページが変更された
	"nicoMylistGroupListLoaded",    // マイリストグループの一覧が読み込まれた
	"nicoMylistGroupListSorted",    // マイリストグループの一覧がソートされた
	"nicoMylistGroupCreated",       // マイリストグループが新規作成された
	"nicoMylistGroupUpdated",       // マイリストグループが更新された
	"nicoMylistGroupRemoved",       // マイリストグループが削除された
	"nicoMylistGroupEditOpened",    // マイリストグループの編集フォームが開かれた
	"nicoMylistGroupEditClosed",    // マイリストグループの編集フォームが閉じられた
	"nicoDeflistLoaded",            // デフォルトマイリストが読み込まれた
	"nicoDeflistUpdated",           // デフォルトマイリストのアイテムが更新された
	"nicoDeflistMoved",             // デフォルトマイリストのアイテムが移動された
	"nicoDeflistCopied",            // デフォルトマイリストのアイテムがコピーされた
	"nicoDeflistRemoved",           // デフォルトマイリストのアイテムが削除された
	"nicoMylistLoaded",             // マイリストが読み込まれた
	"nicoMylistUpdated",            // マイリストのアイテムが更新された
	"nicoMylistMoved",              // マイリストのアイテムが移動された
	"nicoMylistCopied",             // マイリストのアイテムがコピーされた
	"nicoMylistRemoved",            // マイリストのアイテムが削除された
	"nicoMymemoryLoaded",           // マイメモリーが読み込まれた
	"nicoMymemoryRemoved",          // マイメモリーのアイテムが削除された
	"nicoGlobalError",              // エラーが発生した
	"nicoGlobalWarning",            // 警告が発生した
	"nicoGlobalInfo"                // 情報が発生した
], function (i, evt) {
	$.fn[evt] = function (f) {
		return this.bind(evt, f);
	};
});

/* モデル */

var MylistGroup = {

	preload: function (mylistgroups) {
		var self = this;
		my.groups = mylistgroups;
		$.each(my.groups, function (i, group) {
			my.group[group.id] = self._fixGroup(group);
		});
		$(function () {
			$.event.trigger("nicoMylistGroupListLoaded", [my.groups]);
		});
	},

	preloadSingle: function (groupId, group) {
		my.group[groupId] = this._fixGroup(group);
	},

	load: function (callback) {
		if (this.maintenance) {
			my.groups = [];
			$.event.trigger("nicoMylistGroupListLoaded", [my.groups]);
			callback && callback(my.groups);
			return;
		}
		var self = this;
		if (!my.groups) {
			NicoAPI.MylistGroup.list().success(function (data) {
				my.groups = data.mylistgroup;
				$.each(my.groups, function (i, group) {
					my.group[group.id] = self._fixGroup(group);
				});
				$.event.trigger("nicoMylistGroupListLoaded", [my.groups]);
				callback && callback(my.groups);
			});
		} else {
			callback && callback(my.groups);
		}
	},

	get: function (groupId, callback) {
		if (this.maintenance) {
			$.event.trigger("nicoGlobalError", [messages.maintenance]);
			return;
		}
		if (!my.group[groupId]) {
			var self = this;
			NicoAPI.MylistGroup.get(groupId).success(function (data) {
				var group = self._fixGroup(data.mylistgroup);
				self._updateGroup(group);
				callback && callback(group);
			});
		} else {
			callback && callback(my.group[groupId]);
		}
	},

	update: function (groupId, dict, callback) {
		if (this.maintenance) {
			$.event.trigger("nicoGlobalError", [messages.maintenance]);
			return;
		}
		var self = this;
		NicoAPI.MylistGroup.update(
			groupId, dict.name, dict.description,
			dict["public"], dict.default_sort, dict.icon_id
		).success(function () {
			var newGroup = my.group[groupId] || {};
			for (var key in dict) {
				if (dict[key] === undefined)
					continue;
				if (key == "name" || key == "description")
					newGroup[key] = String.interpret(dict[key]).escapeHTML();
				else
					newGroup[key] = dict[key];
			}
			newGroup = self._fixGroup(newGroup);
			self._updateGroup(newGroup);
			callback && callback(newGroup);
		});
	},

	create: function (callback) {
		if (this.maintenance) {
			$.event.trigger("nicoGlobalError", [messages.maintenance]);
			return;
		}
		var self = this;
		this.load(function (groups) {
			var maximum = 0, sort_order = 0, match;
			var mask = new RegExp(config.new_mylistgroup_name
						.replace(/([.\\+*?\[^\]$(){}=!<>|:-])/g, "\\$1")
						.replace(/%d/, "(\\d+)"));
			$.each(groups, function (i, group) {
				if (match = mask.exec(group.name)) {
					maximum = Math.max(maximum, parseInt(match[1]));
				}
				sort_order = Math.max(sort_order, parseInt(group.sort_order) || 0);
			});
			var name = config.new_mylistgroup_name.replace(/%d/, maximum + 1);
			var description;
			if (!my.groups || my.groups.length == 0) {
				description = config.first_mylistgroup_description;
			}
			NicoAPI.MylistGroup.add(name, description, undefined, 1).success(function (data) {
				var newGroup = {
					id: data.id,
					name: name,
					description: description,
					public: 0,
					default_sort: 1,
					create_time: (new Date()).getTime(),
					update_time: (new Date()).getTime(),
					sort_order: sort_order + 1,
					icon_id: 0
				};
				my.groups.unshift(newGroup);
				my.group[newGroup.id] = newGroup;
				$.event.trigger("nicoMylistGroupCreated", [newGroup]);
				callback && callback(newGroup);
			});
		});
	},

	remove: function (groupId, callback) {
		if (this.maintenance) {
			$.event.trigger("nicoGlobalError", [messages.maintenance]);
			return;
		}
		var self = this;
		this.load(function (groups) {
			NicoAPI.MylistGroup.remove(groupId).success(function (data) {
				my.groups = $.grep(my.groups, function (group) {
					return (group.id != groupId);
				});
				delete my.group[groupId];
				$.event.trigger("nicoMylistGroupRemoved", [groupId]);
				callback && callback();
			});
		});
	},

	updateOrder: function (groupIds, callback) {
		if (this.maintenance) {
			$.event.trigger("nicoGlobalError", [messages.maintenance]);
			return;
		}
		if (groupIds.length == 0) {
			return callback && callback();
		}
		NicoAPI.MylistGroup.sort(groupIds).success(function (data) {
			if (my.groups) {
				var newGroups = [];
				$.each(groupIds, function (i, groupId) {
					newGroups.push(my.group[groupId]);
				});
				my.groups = newGroups;
				$.event.trigger("nicoMylistGroupListSorted", [my.groups]);
			}
			callback && callback();
		});
	},

	_updateGroup: function (group) {
		my.group[group.id] = group;
		$.each(my.groups, function (i, g) {
			if (g.id == group.id) {
				my.groups[i] = group;
				return false;
			}
		});
		$.event.trigger("nicoMylistGroupUpdated", [group]);
	},

	_fixGroup: function (group) {
		group["id"] = parseInt(group["id"]);
		group["public"] = (group["public"] == "1");
		group["default_sort"] = parseInt(group["default_sort"]) || 0;
		group["sort_order"] = parseInt(group["sort_order"]) || 0;
		group["icon_id"] = parseInt(group["icon_id"]) || 0;
		return group;
	}

};

var Deflist = {

	preload: function (deflist) {
		my.deflist = deflist;
		$(function () {
			$.event.trigger("nicoDeflistLoaded", [my.deflist]);
		});
	},

	list: function (callback) {
		if (this.maintenance) {
			callback && callback(my.deflist = []);
			return;
		}
		if (!my.deflist) {
			NicoAPI.Deflist.list().success(function (data) {
				my.deflist = data.mylistitem;
				$.event.trigger("nicoDeflistLoaded", [my.deflist]);
				callback && callback(my.deflist);
			});
		} else {
			callback && callback(my.deflist);
		}
	},

	update: function (itemType, itemId, description, callback) {
		if (this.maintenance) {
			$.event.trigger("nicoGlobalError", [messages.maintenance]);
			return;
		}
		NicoAPI.Deflist.update(itemType, itemId, description).success(function (data) {
			description =  String.interpret(description).escapeHTML();
			if (my.deflist) {
				$.each(my.deflist, function (i, item) {
					if (item.item_type == itemType && item.item_id == itemId) {
						item.description = description;
						return false;
					}
				});
			}
			$.event.trigger("nicoDeflistUpdated", [itemType, itemId, description]);
			callback && callback();
		});
	},

	move: function (targetGroupId, idMap, callback) {
		if (this.maintenance) {
			$.event.trigger("nicoGlobalError", [messages.maintenance]);
			return;
		}
		this.list(function (items) {
			NicoAPI.Deflist.moveMulti(targetGroupId, idMap).success(function (data) {
				my.deflist = removeItemsByAPIResult(items, data);
				if (my.currentItems === items) my.currentItems = my.deflist;
				delete my.mylist[targetGroupId];
				$.event.trigger("nicoDeflistMoved", [targetGroupId, idMap, data]);
				callback && callback(data);
			});
		});
	},

	copy: function (targetGroupId, idMap, callback) {
		if (this.maintenance) {
			$.event.trigger("nicoGlobalError", [messages.maintenance]);
			return;
		}
		this.list(function (items) {
			NicoAPI.Deflist.copyMulti(targetGroupId, idMap).success(function (data) {
				delete my.mylist[targetGroupId];
				$.event.trigger("nicoDeflistCopied", [targetGroupId, idMap, data]);
				callback && callback(data);
			});
		});
	},

	remove: function (idMap, callback) {
		if (this.maintenance) {
			$.event.trigger("nicoGlobalError", [messages.maintenance]);
			return;
		}
		this.list(function (items) {
			NicoAPI.Deflist.removeMulti(idMap).success(function (data) {
				my.deflist = removeItemsByIdMap(items, idMap);
				if (my.currentItems === items) my.currentItems = my.deflist;
				$.event.trigger("nicoDeflistRemoved", [idMap]);
				callback && callback(data.delete_count);
			});
		});
	}

};

var Mylist = {

	preload: function (groupId, mylistitem) {
		my.mylist[groupId] = mylistitem;
		$(function () {
			$.event.trigger("nicoMylistLoaded", [mylistitem]);
		});
	},

	list: function (groupId, callback) {
		if (this.maintenance) {
			callback && callback(my.mylist[groupId] = []);
			return;
		}
		if (!my.mylist[groupId]) {
			NicoAPI.Mylist.list(groupId).success(function (data) {
				my.mylist[groupId] = data.mylistitem;
				$.event.trigger("nicoMylistLoaded", [data.mylistitem]);
				callback && callback(data.mylistitem);
			});
		} else {
			callback && callback(my.mylist[groupId]);
		}
	},

	update: function (groupId, itemType, itemId, description, callback) {
		if (this.maintenance) {
			$.event.trigger("nicoGlobalError", [messages.maintenance]);
			return;
		}
		NicoAPI.Mylist.update(groupId, itemType, itemId, description).success(function (data) {
			description =  String.interpret(description).escapeHTML();
			if (my.mylist[groupId]) {
				$.each(my.mylist[groupId], function (i, item) {
					if (item.item_type == itemType && item.item_id == itemId) {
						item.description = description;
						return false;
					}
				});
			}
			$.event.trigger("nicoMylistUpdated", [groupId, itemType, itemId, description]);
			callback && callback();
		});
	},

	move: function (groupId, targetGroupId, idMap, callback) {
		if (this.maintenance) {
			$.event.trigger("nicoGlobalError", [messages.maintenance]);
			return;
		}
		this.list(groupId, function (items) {
			NicoAPI.Mylist.moveMulti(groupId, targetGroupId, idMap).success(function (data) {
				my.mylist[groupId] = removeItemsByAPIResult(items, data);
				if (my.currentItems === items) my.currentItems = my.mylist[groupId];
				delete my.mylist[targetGroupId];
				$.event.trigger("nicoMylistMoved", [groupId, targetGroupId, idMap, data]);
				callback && callback(data);
			});
		});
	},

	copy: function (groupId, targetGroupId, idMap, callback) {
		if (this.maintenance) {
			$.event.trigger("nicoGlobalError", [messages.maintenance]);
			return;
		}
		this.list(groupId, function (items) {
			NicoAPI.Mylist.copyMulti(groupId, targetGroupId, idMap).success(function (data) {
				delete my.mylist[targetGroupId];
				$.event.trigger("nicoMylistCopied", [groupId, targetGroupId, idMap, data]);
				callback && callback(data);
			});
		});
	},

	remove: function (groupId, idMap, callback) {
		if (this.maintenance) {
			$.event.trigger("nicoGlobalError", [messages.maintenance]);
			return;
		}
		this.list(groupId, function (items) {
			NicoAPI.Mylist.removeMulti(groupId, idMap).success(function (data) {
				my.mylist[groupId] = removeItemsByIdMap(items, idMap);
				if (my.currentItems === items) my.currentItems = my.mylist[groupId];
				$.event.trigger("nicoMylistRemoved", [groupId, idMap]);
				callback && callback(data.delete_count);
			});
		});
	}

};

var Mymemory = {

	preload: function (mymemory) {
		my.mymemory = mymemory;
		$(function () {
			$.event.trigger("nicoMymemoryLoaded", [my.mymemory]);
		});
	},

	list: function (callback) {
		if (!my.mymemory) {
			NicoAPI.Mymemory.list().success(function (data) {
				my.mymemory = data.mymemory;
				$.event.trigger("nicoMymemoryLoaded", [my.mymemory]);
				callback && callback(my.mymemory);
			});
		} else {
			callback && callback(my.mymemory);
		}
	},

	remove: function (idMap, callback) {
		this.list(function (items) {
			NicoAPI.Mymemory.removeMulti(idMap).success(function (data) {
				my.mymemory = removeItemsByIdMap(items, idMap);
				if (my.currentItems === items) my.currentItems = my.mymemory;
				$.event.trigger("nicoMymemoryRemoved", [idMap]);
				callback && callback(data.delete_count);
			});
		});
	},

	gotoEditPage: function (itemId) {
		location.href = "/watch/" + itemId + "?edit=mymemory";
	}

};

/* コントローラ */

var Page = {

	MYLIST: "mylist",
	DEFLIST: "deflist",
	MYMEMORY: "mymemory",
	MAINTENANCE: "maintenance",

	dispatch: function (path) {
		if (!my.groups) {
			MylistGroup.load(function () {
				Page.dispatch(path);
			});
			return;
		}

		var pageId = path[0] || "home", groupId;
		switch (pageId) {
		case "home":
		case Page.DEFLIST:
			this.showDeflist();
			return;
		case Page.MYMEMORY:
			this.showMymemory();
			return;
		}

		if (groupId = parseInt(pageId)) {
			switch (path[1]) {
			case 'edit':
				this.editMylist(groupId);
				break;
			default:
				this.showMylist(groupId);
				break;
			}
		}
	},

	showDeflist: function (callback) {
		if (Deflist.maintenance) {
			$.event.trigger("nicoPageChanged", [Page.MAINTENANCE, null, null]);
			return;
		}
		Deflist.list(function (items) {
			my.currentPage = Page.DEFLIST;
			my.currentGroup = null;
			my.currentItems = items;
			$.event.trigger("nicoPageChanged", [Page.DEFLIST, null, items]);
			callback && callback(items);
		});
	},

	showMymemory: function (callback) {
		Mymemory.list(function (items) {
			my.currentPage = Page.MYMEMORY;
			my.currentGroup = null;
			my.currentItems = items;
			$.event.trigger("nicoPageChanged", [Page.MYMEMORY, null, items])
			callback && callback(items);
		});
	},

	showMylist: function (groupId, callback) {
		if (Mylist.maintenance) {
			$.event.trigger("nicoPageChanged", [Page.MAINTENANCE, null, null]);
			return;
		}
		Mylist.list(groupId, function (items) {
			if (!my.group[groupId]) {
				$.event.trigger("nicoGlobalError", [messages.nonexist_mylistgroup]);
				return;
			}
			my.currentPage = Page.MYLIST;
			my.currentItems = items;
			var group = my.currentGroup = my.group[groupId];
			$.event.trigger("nicoPageChanged", [Page.MYLIST, group, items]);
			callback && callback(group, items);
		});
	},

	gotoShowDeflist: function () {
		$.hashpath(["home"]);
	},
	gotoShowMylist: function (groupId) {
		$.hashpath([groupId]);
	},
	gotoEditMylist: function (groupId, create) {
		$.hashpath([groupId, "edit"], { create: create ? 1 : "" });
	},

	editMylist: function (groupId) {
		this.showMylist(groupId, function (group) {
			$.event.trigger("nicoMylistGroupEditOpened", [group]);
		});
	},
	closeEditMylist: function (group) {
		$.event.trigger("nicoMylistGroupEditClosed", [group]);
		this.gotoShowMylist(group.id);
	}

};

/* 初期化処理 */

$(function () {

	$("#SYS_box_loading")
	.nicoApiStart(function () {
		$(this).show();
	})
	.nicoApiStop(function () {
		$(this).hide();
	});

	var timer = null;
	function updateMessage(message, className, nearbyElement) {
		if (className == "error") {
			alert(message);
		} else {
			if (timer) clearTimeout(timer);
			var box = $("#SYS_box_message");
			$("p", box).html(message.replace(/\n/g, "<br>"));
			box.attr("class", className);

			if (nearbyElement) {
				var top = $(nearbyElement).offset().top - box.parent().offset().top;
				box.css({ top: top + "px" });
			} else {
				var top = $.scrollTop() - box.parent().offset().top;
				box.css({ top: Math.max(0, top) + "px" });
			}
			box.fadeIn("normal");
			timer = setTimeout(function () {
				box.fadeOut("normal");
			}, config.message_stay_time);
		}
	}
	$("#SYS_box_message")
	.nicoGlobalError(function (e, message, nearbyElement) {
		updateMessage(message, "error", nearbyElement);
	})
	.nicoGlobalWarning(function (e, message, nearbyElement) {
		updateMessage(message, "warning", nearbyElement);
	})
	.nicoGlobalInfo(function (e, message, nearbyElement) {
		updateMessage(message, "info", nearbyElement);
	})
	.nicoApiError(function () {
		var message = messages.api_failure["UNKNOWN"];
		updateMessage(message, "error");
	})
	.nicoApiFail(function (event, xhr, data, code) {
		var message = messages.api_failure[code] || messages.api_failure["UNKNOWN"];
		updateMessage(message, "error");
	});

	$("#SYS_box_mylistgroups")
	.nicoMylistGroupListLoaded(function (event, groups) {
		$(this)
			.html( $("#tpl_mylistgroups").jarty() )
			.trigger("nicoPageChanged", [my.currentPage, my.currentGroup]);
		$("#SYS_btn_create_group").click(function () {
			MylistGroup.create(function (group) {
				Page.gotoEditMylist(group.id, true);
			});
		});
		$("#SYS_btn_sort_groups").click(function () {
			$("#SYS_box_mylistgroups")
				.html($("#tpl_mylistgroups_order").jarty())
				.trigger("nicoPageChanged", [my.currentPage, my.currentGroup]);
			var g = $("#SYS_box_order_mylistgroups");
			$("select[name='sort']", g).change(function () {
				var value = parseInt($(this).val());
				if (!isNaN(value) && groupsorts[value]) {
					var tmpGroups = Array.prototype.slice.apply(groups);
					tmpGroups.sort(groupsorts[value]);
					$.each(tmpGroups, function (i, group) {
						var item = $("#SYS_box_group_" + group.id);
						item.appendTo(item.get(0).parentNode);
					});
				}
			});
			g.submit(function () {
				var groupIds = [];
				$("#SYS_box_group_table tr").each(function () {
					var match = /^SYS_box_group_(\d+)$/.exec(this.id);
					if (match) groupIds.push(match[1]);
				});
				MylistGroup.updateOrder(groupIds);
				return false;
			})
			.find("input[name='cancel']")
				.click(function () {
					$("#SYS_box_mylistgroups")
						.trigger("nicoMylistGroupListLoaded", [my.groups]);
				});
			$("#SYS_box_group_table").tableDnD({ onDragClass: "dragging" });
		});
	})
	.nicoPageChanged(function (event, p, group) {
		$("a.menu1_in", this).removeClass("menu1_in").addClass("menu1");
		$("a.folder_1", this).removeClass("folder_1").addClass("folder_0");
		switch (p) {
			case Page.MYLIST:
				$("#SYS_box_group_" + group.id + " a.folder_0")
					.removeClass("folder_0").addClass("folder_1");
				break;
			case Page.DEFLIST:
				$("#SYS_btn_deflist")
					.removeClass("menu1").addClass("menu1_in");
				break;
			case Page.MYMEMORY:
				$("#SYS_btn_mymemory")
					.removeClass("menu1").addClass("menu1_in");
				break;
		}
	})
	.nicoMylistGroupUpdated(function (event, group) {
		$(this).trigger("nicoMylistGroupListLoaded", [my.groups]);
		if (my.currentGroup && group.id == my.currentGroup.id)
			$(this).trigger("nicoPageChanged", [my.currentPage, my.currentGroup, my.currentItems]);
	})
	.nicoMylistGroupCreated(function (event, group) {
		$(this).trigger("nicoMylistGroupUpdated", [group]);
	})
	.nicoMylistGroupRemoved(function (event, groupId) {
		$(this).trigger("nicoMylistGroupListLoaded", [my.groups]);
	})
	.nicoMylistGroupListSorted(function (event, groups) {
		$(this).trigger("nicoMylistGroupListLoaded", [groups]);
	});

	$("#SYS_box_mylist_header")
	.nicoPageChanged(function (event, p, group, items) {
		var template;
		switch (p) {
			case Page.MYLIST: template = "#tpl_mylist_header"; break;
			case Page.DEFLIST: template = "#tpl_deflist_header"; break;
			case Page.MYMEMORY: template = "#tpl_mymemory_header"; break;
			case Page.MAINTENANCE: template = "#tpl_maintenance_header"; break;
		}
		if (!template) return;

		$(this).html( $(template).jarty({ group: group, items: items, page_name: p }) );
		if (group) {
			$("#SYS_btn_edit_group").click(function () {
				Page.gotoEditMylist(group.id);
				return false;
			});
			$("#SYS_btn_remove_group").click(function () {
				if (!confirm(messages.confirm_remove_mylistgroup)) return false;
				MylistGroup.remove(group.id, function () {
					Page.gotoShowDeflist();
				});
				return false;
			});
		}

		var headerY = $(this).offset().top;
		if (headerY < $.scrollTop()) {
			window.scrollTo(0, headerY);
		}

		var pageTitle = $("h1", this).text() || "";
		if (matches = config.title_matcher.exec(document.title)) {
			document.title = pageTitle + matches[1];
		}
	})
	.nicoMylistGroupEditClosed(function (event, group) {
		$(this).trigger("nicoPageChanged", [my.currentPage, my.currentGroup, my.currentItems]);
	})
	.nicoMylistGroupEditOpened(function (event, group) {
		var create = $.hashquery("create");
		var icon_id = group.icon_id || 0;
		$(this).html(
			$("#tpl_mylist_header_edit").jarty({ group: group, items: my.mylist[group.id], create: create })
		);
		$("#SYS_box_group_edit").submit(function () {
			var dict = { icon_id: icon_id };
			$.each($(this).serializeArray(), function (i, p) {
				dict[p.name] = p.value;
			});
			MylistGroup.update(group.id, dict, function () {
				Page.closeEditMylist(group);
			});
			return false;
		});
		$("#SYS_btn_cancel_edit_group").click(function () {
			Page.closeEditMylist(group);
		});
		$("#SYS_box_choose_icon a.folder_0").click(function () {
			icon_id = $(this).prevAll().length;
			$("#SYS_box_choose_icon a.folder_0").removeClass("selected");
			$(this).addClass("selected");
		});
		if (create)
			$("#SYS_box_group_edit input[name='name']").focus();
	});

	var page_load_timer;

	$("#SYS_box_mylist_body")
	.nicoPageChanged(function (event, p, group, items) {
		if (page_load_timer) {
			clearInterval(page_load_timer);
			delete page_load_timer;
		}
		if (p == Page.MAINTENANCE) {
			$(this).empty();
			return;
		}

		var query = $.hashquerydict();
		var page = Math.max(1, parseInt(query.page) || 1);
		var sort = parseInt(query.sort);

		var filters = [];
		$.each((query.filters || "").split(/,/),
			function (i, k) {
				k = parseInt(k);
				if (itemtypes[k]) filters[k] = true;
			});
		if (filters.length > 0) {
			items = $.grep(items, function (item) {
				return !filters[item.item_type];
			});
		}

		if (isNaN(sort)) {
			sort = (group ? group.default_sort : 1) || 0;
		}

		if (items.sortedBy !== sort && itemsorts[sort]) {
			items.sort(itemsorts[sort]);
			items.sortedBy = sort;
		}

		var page_items = items.slice(
			(page-1) * config.items_per_page,
			page * config.items_per_page
		);
		if (page_items.length == 0 && page > 1) {
			var new_page = Math.max(1, Math.ceil(items.length / config.items_per_page));
			$.hashquery("page", new_page);
			return false;
		}

		var template;
		switch (p) {
			case Page.MYLIST: template = "#tpl_mylist_body"; break;
			case Page.DEFLIST: template = "#tpl_deflist_body"; break;
			case Page.MYMEMORY: template = "#tpl_mymemory_body"; break;
		}
		if (!template) return;

		$(this).html(
			$(template).jarty({
				group: group, items: items, any_page_items: (page_items.length > 0),
				page: page, sort: sort, filters: filters,
				page_name: p })
		);

		var self = this;
		$(".SYS_btn_pager", this).click(function () {
			$.hashquery("page", $(this).text());
			return false;
		});
		$(".SYS_btn_pager_prev", this).click(function () {
			$.hashquery("page", Math.max(page - 1, 1));
			return false;
		});
		$(".SYS_btn_pager_next", this).click(function () {
			$.hashquery("page", page + 1);
			return false;
		});
		$(".SYS_box_sortpager", this).each(function () {
			var form = $(this);
			$("input[name='filters']", this).click(function () {
				var filters = form.find("input[name='filters']").not(":checked")
					.map(function () { return $(this).val() });
				$.hashquerydict({
					filters: $.makeArray(filters).join(","),
					page: ""
				}, true);
			});
			$("select[name='sort']", this).change(function () {
				var sort = $(this).val();
				$.hashquerydict({
					sort: sort,
					page: ""
				}, true);
			});
		});
		$("#SYS_box_check_editor", this).each(function () {
			var form = this;
			var collectIdMap = function () {
				var checked = $(".SYS_box_item:has(input[name='checkbox']:checked)", self);
				if (checked.length == 0) return false;

				var id_map = {};
				checked.each(function () {
					var match = this.id.match(/^SYS_box_item_(\d+)_(.+)$/);
					if (match) {
						id_map[match[1]] = id_map[match[1]] || [];
						id_map[match[1]].push(match[2]);
					}
				});
				return id_map;
			};
			var moveCallback = function (data) {
				var dups = data.duplicates.item || [], mats = data.matches.item || [], tags = data.targets.item || [];
				if (mats.length > 0 && mats.length == dups.length) {
					var message = messages.move_duplicates({ total: mats.length, dup: dups.length });
					$.event.trigger("nicoGlobalError", [message, "#SYS_box_check_editor"]);
				} else if (dups.length > 0) {
					var message = messages.move_duplicates({ total: mats.length, dup: dups.length });
					$.event.trigger("nicoGlobalWarning", [message, "#SYS_box_check_editor"]);
				} else if (tags.length > 0) {
					var message = messages.move_success({ total: tags.length });
					$.event.trigger("nicoGlobalInfo", [message, "#SYS_box_check_editor"]);
				}
				$("input[name='checkbox']:checked", self).attr("checked", false);
			};
			var copyCallback = function (data) {
				var dups = data.duplicates.item || [], mats = data.matches.item || [], tags = data.targets.item || [];
				if (mats.length > 0 && mats.length == dups.length) {
					var message = messages.copy_duplicates({ total: mats.length, dup: dups.length });
					$.event.trigger("nicoGlobalError", [message, "#SYS_box_check_editor"]);
				} else if (dups.length > 0) {
					var message = messages.copy_duplicates({ total: mats.length, dup: dups.length });
					$.event.trigger("nicoGlobalWarning", [message, "#SYS_box_check_editor"]);
				} else if (tags.length > 0) {
					var message = messages.copy_success({ total: tags.length });
					$.event.trigger("nicoGlobalInfo", [message, "#SYS_box_check_editor"]);
				}
				$("input[name='checkbox']:checked", self).attr("checked", false);
			};
			var removeCallback = function (count) {
				if (count > 0) {
					var message = messages.remove_success({ total: count });
					$.event.trigger("nicoGlobalInfo", [message, "#SYS_box_check_editor"]);
				}
			};
			$("#SYS_btn_move_mylist", this).click(function () {
				var id_map = collectIdMap();
				if (id_map) {
					var group_id = $("[name='target_group_id']", form).val();
					switch (p) {
					case Page.MYLIST:
						Mylist.move(my.currentGroup.id, group_id, id_map, moveCallback);
						break;
					case Page.DEFLIST:
						Deflist.move(group_id, id_map, moveCallback);
						break;
					}
				}
				return false;
			});
			$("#SYS_btn_copy_mylist", this).click(function () {
				var id_map = collectIdMap();
				if (id_map) {
					var group_id = $("[name='target_group_id']", form).val();
					switch (p) {
					case Page.MYLIST:
						Mylist.copy(my.currentGroup.id, group_id, id_map, copyCallback);
						break;
					case Page.DEFLIST:
						Deflist.copy(group_id, id_map, copyCallback);
						break;
					}
				}
				return false;
			});
			$("#SYS_btn_remove_mylist", this).click(function () {
				if (!confirm(messages.confirm_remove_mylist)) return false;
				var id_map = collectIdMap();
				if (id_map) {
					switch (p) {
					case Page.MYLIST:
						Mylist.remove(my.currentGroup.id, id_map, removeCallback);
						break;
					case Page.DEFLIST:
						Deflist.remove(id_map, removeCallback);
						break;
					case Page.MYMEMORY:
						Mymemory.remove(id_map, removeCallback);
						break;
					}
				}
				return false;
			});
		});

		var list_box = $("#SYS_page_items", this);
		list_box.empty();

		var appendItem = function (index) {
			list_box.append($("#tpl_mylistitem").jarty({
				item: page_items[index],
				show_memo: (p != Page.MYMEMORY),
				group: group, items: items, page: page, sort: sort, page_name: p
			}));
			var item = list_box.children(":last").get(0);
			$("input[name='checkbox']", item).click(function () {
				if (this.checked) {
					$("#SYS_box_check_editor input:button").removeClass("disabled");
				} else {
					if ($("input[name='checkbox']:checked", self).length == 0)
						$("#SYS_box_check_editor input:button").addClass("disabled");
				}
			});
			var editting = false;
			$(".SYS_box_item_data", item).hover(function () {
				if (!editting) $(".SYS_box_item_buttons", item).show();
			}, function () {
				if (!editting) $(".SYS_box_item_buttons", item).hide();
			});
			$(".SYS_btn_edit_memo", item).click(function () {
				editting = true;
				$(".SYS_box_item_buttons", item).hide();
				$(".SYS_box_memo_normal", item).hide();
				$(".SYS_box_memo_edit", item).show()
					.find("textarea[name='memo']").focus();
			});
			$(".SYS_box_memo_edit", item).submit(function () {
				var match = item.id.match(/^SYS_box_item_(\d+)_(.+)$/);
				if (!match) return;
				var newDescription = $("textarea[name='memo']", item).val();
				var commitCallback = function () {
					editting = false;
					var htmlDescription = newDescription.escapeHTML().replace(/\n/g, "<br>");
					$(".SYS_box_memo_edit", item).hide();
					$(".SYS_box_memo_normal", item).toggle(htmlDescription != "")
						.find(".SYS_box_memo")
							.html(htmlDescription)
							.toggle(htmlDescription.length > 0);
				};
				switch (p) {
				case Page.MYLIST:
					Mylist.update(my.currentGroup.id, match[1], match[2], newDescription, commitCallback);
					break;
				case Page.DEFLIST:
					Deflist.update(match[1], match[2], newDescription, commitCallback);
					break;
				}
				return false;
			});
			$(".SYS_btn_cancel_edit_memo", item).click(function () {
				editting = false;
				$(".SYS_box_memo_edit", item).hide();
				$(".SYS_box_memo_normal", item).toggle(
					$(".SYS_box_memo", item).text() != "");
			});
			$(".SYS_btn_remove_item", item).click(function () {
				if (!confirm(messages.confirm_remove_mylist)) return;
				var match = item.id.match(/^SYS_box_item_(\d+)_(.+)$/);
				if (match) {
					var removeCallback = function (count) {
						if (count > 0) {
							var message = messages.remove_success({ total: count });
							$.event.trigger("nicoGlobalInfo", [message]);
						}
					};
					var id_map = { }; id_map[match[1]] = [ match[2] ];
					switch (p) {
					case Page.MYLIST:
						Mylist.remove(my.currentGroup.id, id_map, removeCallback);
						break;
					case Page.DEFLIST:
						Deflist.remove(id_map, removeCallback);
						break;
					case Page.MYMEMORY:
						Mymemory.remove(id_map, removeCallback);
						break;
					}
				}
			});
			$(".SYS_btn_edit_mymemory", item).click(function () {
				var match = item.id.match(/^SYS_box_item_(\d+)_(.+)$/);
				if (!match) return;
				Mymemory.gotoEditPage(match[2]);
			});
			return item;
		}

		var index = 0,
			images = [], addImage = function (i, img) { images.push(img) },
			isFirst = true;
		page_load_timer = setInterval(function () {
			for (var i = 0; i < 10; i++) {
				if (index >= page_items.length) break;
				var item = appendItem(index++);
				if (!isFirst) $("img.lazyimage", item).each(addImage);
			}
			if (isFirst) {
				Nico.LazyImage.reset();
				isFirst = false;
			}
			if (index >= page_items.length) {
				if (images.length > 0) {
					Nico.LazyImage.enqueue(images);
				}
				clearInterval(page_load_timer);
				delete page_load_timer;
				return;
			}
		}, 10);
	})
	.nicoDeflistMoved(function (event, targetGroupId, idMap, data) {
		$(this).trigger("nicoPageChanged", [my.currentPage, my.currentGroup, my.currentItems]);
		if (data && data.duplicates && data.duplicates.item) {
			$.each(data.duplicates.item, function (i, item) {
				$("#SYS_box_item_" + item.type + "_" + item.id + " input[name='checkbox']")
					.attr("checked", true);
			});
		}
	})
	.nicoDeflistRemoved(function () {
		$(this).trigger("nicoPageChanged", [my.currentPage, my.currentGroup, my.currentItems]);
	})
	.nicoMylistMoved(function (event, groupId, targetGroupId, idMap, data) {
		$(this).trigger("nicoPageChanged", [my.currentPage, my.currentGroup, my.currentItems]);
		if (data && data.duplicates && data.duplicates.item) {
			$.each(data.duplicates.item, function (i, item) {
				$("#SYS_box_item_" + item.type + "_" + item.id + " input[name='checkbox']")
					.attr("checked", true);
			});
		}
	})
	.nicoMylistRemoved(function (event, groupId) {
		$(this).trigger("nicoPageChanged", [my.currentPage, my.currentGroup, my.currentItems]);
	})
	.nicoMymemoryRemoved(function () {
		$(this).trigger("nicoPageChanged", [my.currentPage, my.currentGroup, my.currentItems]);
	});


	/* location.hash によるページ遷移 */

	var lastHash;
	$(window).history(function (event, hash) {
		if (lastHash != location.hash) {
			lastHash = location.hash;
			Page.dispatch($.hashpath());
		}
	});
	lastHash = location.hash;
	Page.dispatch($.hashpath());

});

/* ユーティリティ関数 */

$.extend($, {

	// location.hash からパス部分を取得/設定
	hashpath: function (path, query) {
		if (arguments.length == 0) {
			var match = (location.hash || "#").match(/^#\/([^\+]+)/);
			return match ? match[1].split(/\//) : [];
		}

		var hash = "#";
		if ($.isArray(path)) {
			if (path.length > 0)
				hash += "/" + path.join("/");
		} else {
			if (path)
				hash += path;
		}
		if (arguments.length == 2) {
			if (query = $.param(query).replace(/&/g, "+")
					.replace(/[^=\+]+=(?:\+|$)/g, "").replace(/\++$/g, ""))
				hash += "+" + query;
		}
		return location.hash = hash;
	},

	// location.hash のクエリパラメータ部分を取得/設定
	hashquerydict: function (value, merge) {
		if (arguments.length == 0) {
			var hash = location.hash, dict = {}, re = /[\+]([^=]+)=([^\+]*)/g, m;
			while (m = re.exec(hash))
				dict[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
			return dict;
		} else if (!merge) {
			return $.hashpath($.hashpath(), value);
		} else {
			return $.hashpath($.hashpath(), $.extend($.hashquerydict(), value));
		}
	},

	// location.hash からクエリパラメータの1つを取得/設定
	hashquery: function (name, value) {
		name = encodeURIComponent(name);
		if (arguments.length == 1) {
			var match = (new RegExp('^#[^\\+]*\\+(?:.*\\+)?' + name + '=([^\\+]*)')).exec(location.hash);
			return match ? match[1] : null;
		} else {
			value = encodeURIComponent(value);
			var hash = location.hash || "#";
			var match = (new RegExp('^(#[^\\+]*\\+(?:.*\\+)?)' + name + '=([^\\+]*)')).exec(hash);
			if (match && value === "") {
				hash = match[1] + hash.slice(match[0].length + 1);
			} else if (match) {
				hash = match[1] + name + "=" + value + hash.slice(match[0].length);
			} else if (value !== "") {
				hash += "+" + name + "=" + value;
			}
			return location.hash = hash;
		}
	}

});


// アイテム配列から id_map に存在するアイテムを取り除いて返す.
function removeItemsByIdMap(items, idMap) {
	var linear = {};
	for (var item_type in idMap) {
		linear[item_type] = {};
		$.each(idMap[item_type], function (i, item_id) {
			linear[item_type][item_id] = true;
		});
	}
	return $.grep(items, function (item) {
		return !linear[item.item_type] || !linear[item.item_type][item.item_id];
	});
}

// アイテム配列からAPIの結果で成功したアイテムを取り除いて返す.
function removeItemsByAPIResult(items, data) {
	if (!data || !data.targets || !data.targets.item) return items;
	var linear = {};
	$.each(data.targets.item, function (i, item) {
		linear[item.type] = linear[item.type] || {};
		linear[item.type][item.id] = true;
	});
	return $.grep(items, function (item) {
		return !linear[item.item_type] || !linear[item.item_type][item.item_id];
	});
}

// 指定したプロパティで2つのアイテムを比較する関数を返す
function item_comparer(props, reverse, numeric) {
	var p = function (v) {
		return $.map(props, function (prop) {
			return v + '["' + prop.replace(/\./g, '"]["') + '"]';
		}).join("|");
	};
	return new Function('a,b',
		'var x = ' + p('a') + ', y = ' + p('b') + ';' +
		'if (x === undefined && y !== undefined) return 1;' +
		'if (x !== undefined && y === undefined) return -1;' +
		'if (x === undefined && y === undefined)' +
			'x = a.item_data.title || a.create_time,' +
			'y = b.item_data.title || b.create_time;' +
		(numeric ?
			'else x = parseFloat(x), y = parseFloat(y);' : ''
		) +
		(reverse ?
			'return x > y ? -1 : x < y ? 1 : 0;' :
			'return x < y ? -1 : x > y ? 1 : 0;'
		)
	);
}

// 指定したプロパティで2つのグループを比較する関数を返す
function group_comparer(prop, reverse) {
	prop = '["' + prop.replace(/\./g, '"]["') + '"]';
	return new Function(reverse ? 'a,b' : 'b,a',
		'return a'+prop+' < b'+prop+' ? -1 : a'+prop+' > b'+prop+' ? 1 : 0;'
	);
}

// サイトのURLを取得
function siteURL(site) {
	if (arguments.callee.mapping[site])
		return arguments.callee.mapping[site];
	else
		return "http://" + location.hostname.replace(/^[a-z]+/, site) + "/";
};
siteURL.mapping = {
	"smile": "http://www.smilevideo.jp/",
	"commons": "http://www.niconicommons.jp/"
};

// 説明文の自動リンク用ルール
var formatRules = [
	[ '\\b(?:sm|nm|so|ca|ax|yo|nl|ig|na|cw|z[a-e]|om|sk|yk)\\d{1,14}\\b',
	  function (s) { return "/watch/" + s; } ],
	[ '\\b(?:watch|user|myvideo|mylist(?:/\\d{1,10})?)/\\d{1,10}\\b',
	  function (s) { return "/" + s; } ],
	[ '\\bco\\d{1,14}\\b',
	  function (s) { return siteURL("ch") + "community/" + s; } ],
	[ '\\bch\\d{1,14}\\b',
	  function (s) { return siteURL("ch") + "channel/" + s; } ],
	[ '\\bnc\\d{1,14}\\b',
	  function (s) { return siteURL("commons") + "material/" + s; } ],
	[ '\\b(?:dw\\d+|az[A-Z0-9]{10}|ys[a-zA-Z0-9-]+_[a-zA-Z0-9-]+' +
	             '|ga\\d+|ip[\\d_]+|gg[a-zA-Z0-9]+-[a-zA-Z0-9-]+)\\b',
	  function (s) { return siteURL("ichiba") + "item/" + s; } ],
	[ '\\blv\\d{1,14}\\b',
	  function (s) { return siteURL("live") + "watch/" + s; } ],
	[ '\\bsg\\d{1,14}\\b',
	  function (s) { return siteURL("seiga") + "bbs/" + s; } ]
];
var formatRegexp = new RegExp('(' +
	$.map(formatRules, function (r) { return r[0]; }).join(')|(') + ')');

// 与えられた文字列内の各種IDをHTMLでリンク化して返す.
$.nicoFormat = function (str) {
	var length = formatRules.length;
	return String.interpret(str).gsub(formatRegexp, function (match) {
		var i = 1; while (!match[i] && i <= length) i++;
		return i > length ? '' :
			'<a href="' + formatRules[i-1][1](match[i]) + '">' + match[0] + '</a>';
	});
}
// 要素の html() 内の各種IDをリンク化する.
$.fn.nicoFormat = function () {
	return $(this).each(function () {
		$(this).html($.nicoFormat($(this).text()));
	});
};

// ウィンドウの垂直スクロール位置を取得.
$.scrollTop = function () {
	return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
}

/* Jarty 拡張 */
$.extend(Jarty.Pipe.prototype, {
	// 数値フォーマット.
	numberFormat: function (r, decimals, dec_) {
		this.value = this.stringify().value.replace(/(\d)(?=(?:\d{3})+$)/g, "$1,");
		return this;
	},
	// 再生秒数フォーマット
	timeFormat: function (r) {
		this.value = $.nicoFormat(this.stringify().value);
		var num = parseInt(this.value);
		if (!isNaN(num)) {
			var m = Math.floor(num / 60), s = num % 60;
			if (s < 10) s = "0" + s;
			this.value = m + ":" + s;
		}
		return this;
	},
	// 自動リンク.
	nicoFormat: function (r) {
		this.value = $.nicoFormat(this.stringify().value);
		return this;
	}
});
$.extend(Jarty.Function, {
	// id で指定した要素をテンプレートとして書き込み.
	include: function (r, params) {
		var element = $("#" + (params.file || params.id));
		if (element && element.length > 0) {
			var f = function (o) { for (var k in o) this[k] = o[k]; };
			f.prototype = r.dict;
			r.write(element.jarty(new f(params)));
		}
	}
});


// export
window.my = my;
window.MylistGroup = MylistGroup;
window.Deflist = Deflist;
window.Mylist = Mylist;
window.Mymemory = Mymemory;

my.setPageDispatcher = function (dispatcher) {
	Page.dispatch = dispatcher;
}


})(jQuery);
