// ==UserScript==
// @name        Expand replies tree
// @namespace   http://lowreal.net/
// @include     http://h.hatena.ne.jp/*
// @include     http://h.hatena.com/*
// ==/UserScript==

location.href = "javascript:"+encodeURIComponent(uneval(function () {

(function (Global) { with (D()) {
	const WAIT  = 1;
	const LEVEL = 3;
	const ICON  = [
		"data:image/png;base64,",
		"iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAMAAAAolt3jAAAAD1BMVEX45u/y",
		"zt////+za4X///8WHaQkAAAABXRSTlP/////APu2DlMAAAAZdEVYdFNvZnR3",
		"YXJlAEFkb2JlIEltYWdlUmVhZHlxyWU8AAAARElEQVQI103OORIAQAgCQWD4",
		"/5s3cC+yLgtV5aVVyckmkW0PG8YBNK3IDj0k+vlNX3fzbkb8d++q+WrYVLZd",
		"1JaeDF8WzSMCBiKqHq0AAAAASUVORK5CYII="
	].join("");

	addStyle([
		"div.info .expand a {",
			"background: transparent url('",ICON,"') no-repeat scroll 0 65%;",
			"padding: 6px 0 2px 16px;",
			"margin: 0 0 0 3px",
		"}",
	]);

	function applyJavaScript (entry) {
		entry = entry.parentNode.removeChild(entry);

		// remove event handlers
		entry.innerHTML = entry.innerHTML;

		// append to temporary element
		var t = h();
		t.appendChild(entry);

		// remove duplicated star elements
		$X("../*/div/*[@class='title']/span[@class='hatena-star-comment-container' or @class='hatena-star-star-container']", entry).forEach(function (e) {
			e.parentNode.removeChild(e);
		});

		// remove expand links
		$X("./div/div/span[@class='expand']", entry).forEach(function (i) {
			i.parentNode.removeChild(i);
		});

		// remove script added links
		$X("./div/div/span[@class='delete']/img", entry).forEach(function (i) {
			i.parentNode.removeChild(i);
		});

		Global.Hatena.Haiku.Pager.dispatchEvent('loadedEntries', entry.parentNode.wrappedJSObject || entry.parentNode);
		return entry;
	}

	function expandReplies (entry, level) {
		if (typeof level == "undefined") level = LEVEL;
		log("level:" + level);
		if (entry._expanded) return next();

		if (level < 1) {
			// 深すぎるので、さらに展開するためのリンクをつけたうえで終了する。
			return next(function (i) {
				log([entry, "too deep"]);
				if (!$X("boolean( ./div/div/a[.//img[@alt='Reply to'] and not(@class = '__loaded')] | ./div/div/span[@class='replies']/a[not(@class = '__loaded')] )", entry)) return;
				entry._gm_expandreplies_applied = false;
				addExpandLink(entry, function () {
					log("expand more -> child");
					return expandReplies(entry, LEVEL);
				});
			}).error(function (e) { alert(e) });
		}

		entry._expanded = true;

		return parallel([
			expandChildReplies(entry, level),
			expandParent(entry, level)
		]);

		function expandParent (entry, level) {
			log(["expandParent", entry, level]);

			var a        = $X("./div[@class='list-body']/div/a[img[@alt='Reply to']]", entry)[0];
			var raw_href = $X("string(@href)", a);
			var es       = $X("//div[@class='entry' and ./div/div/span[@class='timestamp']/a[@href = '"+raw_href+"']]");

			if (a) a.style.opacity = "0.6";

			return !a ? next() : es.length ? appendAndLoadNext(es[0]) : http.get(a.href).next(function (data) {
				var t = h(data.responseText.replace(/<\/?(?:script|i?frame)[^>]*>/g, ""));
				return parallel($X(".//div[@class='entries']/div[@class='entry' and not(@class='ad')]", t).map(appendAndLoadNext));
			});

			function appendAndLoadNext (e) {
				if (a) {
					a.style.opacity = "0.2";
					a.className = "__loaded";
				}

				if (e._expanded) return next();

				e = applyJavaScript(e);
				var body = $X("./div[@class='list-body']", e)[0];
				entry.parentNode.insertBefore(e, entry);
				body.appendChild(entry);

				$X("./div[@class='list-body']/div[@class='info']/span[@class='replies']/a[@href]", e).forEach(function (a) {
					// 既にロードされているなら半透明にしとく
					var raw_href = $X("string(@href)", a);
					if ($X("boolean(.//span[@class='timestamp']/a[@href = '"+raw_href+"'])", e)) {
						a.style.opacity = "0.2";
						a.className = "__loaded";
					}
				});

				return wait(WAIT).next(function () {
					return call(expandReplies, e, level - 1);
				});
			}
		}

		function expandChildReplies (entry, level) {
			log(["expandChildReplies", entry, level]);

			var body = $X("./div[@class='list-body']", entry)[0];
			return parallel(
				$X("./div[@class='info']/span[@class='replies']/a[@href]", body).map(function (a) {
					var raw_href = $X("string(@href)", a);

					// don't show if this was showed entry
					if (a.className == "__loaded") return next();

					a.style.opacity = "0.6";

					// don't load already showed entry
					var es = $X("//div[@class='entry' and .//span[@class='timestamp']/a[@href = '"+raw_href+"']]");
					if (es.length) return appendAndLoadNext(es[0]);
					return http.get(a.href).next(function (data) {
						// remove unnecessary tags
						var t = h(data.responseText.replace(/<\/?(?:script|i?frame)[^>]*>/g, ""));
						return parallel($X(".//div[@class='entries']/div[@class='entry' and not(@class='ad')]", t).map(appendAndLoadNext));
					});

					function appendAndLoadNext (e) {
						a.style.opacity = "0.2";
						a.className = "__loaded";

						var parentLink = $X("./div[@class='list-body']/div/a[img[@alt='Reply to']]", e)[0]
						parentLink.style.opacity = "0.2";
						parentLink.className = "__loaded";

						// 時系列に並ぶ様に注意する
						var time = $X("string(div/div/span[@class='timestamp']/a)", e);
						var ref = $X("./div[@class='entry' and not(@class='ad')]", body).filter(function (i) {
							return $X("string(div/div/span[@class='timestamp']/a)", i) < time;
						})[0];

						body.insertBefore(applyJavaScript(e), ref);

						return wait(WAIT).next(function () {
							return call(expandReplies, e, level - 1);
						});
					}
				})
			);
		}
	}

	function addExpandLink (e, handler) {
		if (e._gm_expandreplies_applied) return;
		e._gm_expandreplies_applied = true;

		var info = $X("./div/div[@class='info']", e)[0];
		var link = dom("<span class='expand'><a href='javascript:void(156)'>Expand</a></span>", info).root;
		link.addEventListener("click", function () {
			if (window.getSelection().toString()) return;
			if (e._loading) return;
			e._loading = true;
			log("Loading...");
			link.innerHTML = "loading";
			handler().next(function () {
				link.parentNode.removeChild(link);
			}).error(function (e) { alert(e) });
		}, false);
	}

	// observe pagerize
	var num = 0;
	next(function () {

		var n = $X("count(//div[@class='entries']/div[@class='entry' and not(@class='ad')])");
		if (num < n) {
			var entries = $X("//div[@class='entries']/div[@class='entry' and not(@class='ad')][.//img[@alt='Reply to'] or .//span[@class='replies']/a]");
			loop(entries.length, function (n) {
				var e = entries[n].wrappedJSObject || entries[n];
				addExpandLink(e, function () {
					return expandReplies(e).
					next(function () {
						if (log.element) {
							log.element.parentNode.removeChild(log.element);
							log.element = null;
						}
					}).
					error(function (e) {
						alert("Error"+e);
					});
				});
			});

			num = n;
		}
		return wait(WAIT).next(arguments.callee);
	}).
	error(function (e) {
		log(e);
	});


} // end with

/* template functions  */
function h (s) {
	var d = document.createElement("div");
	d.innerHTML = s;
	return d;
}

function addStyle (a) {
	dom("<style type='text/css'>#{css}</style>", document.body, { css : a.join("") });
}


function dom (str, cur, txt) {
	var t, ret = {}, stack = [cur = cur || document.createElement("div")];
	while (str.length) {
		if (str.indexOf("<") == 0) {
			if (t = str.match(/^\s*<(\/?[^\s>\/]+)([^>]+?)?(\/)?>/)) {
				var tag = t[1], attrs = t[2], isempty = !!t[3];
				if (tag.indexOf("/") == -1) {
					child = document.createElement(tag);
					if (attrs) attrs.replace(/([a-z]+)=(?:'([^']+)'|"([^"]+)")/gi,
						function (m, name, v1, v2) {
							var v = text(v1 || v2);
							if (name == "class") ret[v] = child;
							child.setAttribute(name, v);
						}
					);
					cur.appendChild(ret.root ? child : (ret.root = child));
					if (!isempty) {
						stack.push(cur);
						cur = child;
					}
				} else cur = stack.pop();
			} else throw("Parse Error: " + str);
		} else {
			if (t = str.match(/^([^<]+)/)) cur.appendChild(document.createTextNode(text(t[0])));
		}
		str = str.substring(t[0].length);
	}
	return ret;

	function text (str) {
		return str
			.replace(/&(#(x)?)?([^;]+);/g, function (_, isNumRef, isHex, ref) {
				return isNumRef ? String.fromCharCode(parseInt(ref, isHex ? 16 : 10)):
				                  {"lt":"<","gt":"<","amp":"&"}[ref];
			})
			.replace(/#\{([^}]+)\}/, function (_, name) {
				return txt[name];
			});
	}
}


function log (m) {
	if (Global.console) Global.console.log(m);

//	if (!arguments.callee.element) {
//		arguments.callee.element = h("<div style='position:fixed;top:0;left:0;background:#000;color:#fff;padding:0 0.5em;'/>").firstChild;
//		document.body.appendChild(arguments.callee.element);
//	}
//	arguments.callee.element.innerHTML = escapeHTML(m);
}

function escapeHTML (str) {
	str = String(str);
	var map = { "&" : "&amp;", "<" : "&lt;", ">" : "&gt;"};
	return str.replace(/[&<>]/g, function (m) {
		return map[m];
	});
};



// extend version of $X
// $X(exp);
// $X(exp, context);
// $X(exp, type);
// $X(exp, context, type);
function $X (exp, context, type /* want type */) {
	// @class='' を展開する
	exp = exp.replace(/@class\s*=\s*(?:"([^"]+)"|'([^']+)')/g, "contains(concat(' ', @class, ' '), ' $1$2 ')");

	if (typeof context == "function") {
		type    = context;
		context = null;
	}
	if (!context) context = document;
	exp = (context.ownerDocument || context).createExpression(exp, function (prefix) {
		return document.createNSResolver((context.ownerDocument == null ? context
		                                                                : context.ownerDocument).documentElement)
		               .lookupNamespaceURI(prefix) ||
		       ({ "atom" : "http://purl.org/atom/ns#", "hatena" : "http://www.hatena.ne.jp/info/xmlns#" })[prefix] ||
		       document.documentElement.namespaceURI;
	});

	switch (type) {
		case String:
			return exp.evaluate(
				context,
				XPathResult.STRING_TYPE,
				null
			).stringValue;
		case Number:
			return exp.evaluate(
				context,
				XPathResult.NUMBER_TYPE,
				null
			).numberValue;
		case Boolean:
			return exp.evaluate(
				context,
				XPathResult.BOOLEAN_TYPE,
				null
			).booleanValue;
		case Array:
			var result = exp.evaluate(
				context,
				XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
				null
			);
			var ret = [];
			for (var i = 0, len = result.snapshotLength; i < len; ret.push(result.snapshotItem(i++)));
			return ret;
		case undefined:
			var result = exp.evaluate(context, XPathResult.ANY_TYPE, null);
			switch (result.resultType) {
				case XPathResult.STRING_TYPE : return result.stringValue;
				case XPathResult.NUMBER_TYPE : return result.numberValue;
				case XPathResult.BOOLEAN_TYPE: return result.booleanValue;
				case XPathResult.UNORDERED_NODE_ITERATOR_TYPE: {
					// not ensure the order.
					var ret = [];
					var i = null;
					while (i = result.iterateNext()) {
						ret.push(i);
					}
					return ret;
				}
			}
			return null;
		default:
			throw(TypeError("$X: specified type is not valid type."));
	}
}


// Usage:: with (D()) { your code }
// JSDeferred 0.2.1 (c) Copyright (c) 2007 cho45 ( www.lowreal.net )
// See http://coderepos.org/share/wiki/JSDeferred
function D () {


function Deferred () { return (this instanceof Deferred) ? this.init(this) : new Deferred() }
Deferred.prototype = {
	init : function () {
		this._next    = null;
		this.callback = {
			ok: function (x) { return x },
			ng: function (x) { throw  x }
		};
		return this;
	},

	next  : function (fun) { return this._post("ok", fun) },
	error : function (fun) { return this._post("ng", fun) },
	call  : function (val) { return this._fire("ok", val) },
	fail  : function (err) { return this._fire("ng", err) },

	cancel : function () {
		(this.canceller || function () {})();
		return this.init();
	},

	_post : function (okng, fun) {
		this._next =  new Deferred();
		this._next.callback[okng] = fun;
		return this._next;
	},

	_fire : function (okng, value) {
		var self = this, next = "ok";
		try {
			value = self.callback[okng].call(self, value);
		} catch (e) {
			next  = "ng";
			value = e;
		}
		if (value instanceof Deferred) {
			value._next = self._next;
		} else {
			if (self._next) self._next._fire(next, value);
		}
		return this;
	}
};

Deferred.parallel = function (dl) {
	var ret = new Deferred(), values = {}, num = 0;
	for (var i in dl) if (dl.hasOwnProperty(i)) {
		(function (d, i) {
			d.next(function (v) {
				values[i] = v;
				if (--num <= 0) {
					if (dl instanceof Array) {
						values.length = dl.length;
						values = Array.prototype.slice.call(values, 0);
					}
					ret.call(values);
				}
			}).error(function (e) {
				ret.fail(e);
			});
			num++;
		})(dl[i], i);
	}
	if (!num) Deferred.next(function () { ret.call() });
	ret.canceller = function () {
		for (var i in dl) if (dl.hasOwnProperty(i)) {
			dl[i].cancel();
		}
	};
	return ret;
};

Deferred.wait = function (n) {
	var d = new Deferred(), t = new Date();
	var id = setTimeout(function () {
		clearTimeout(id);
		d.call((new Date).getTime() - t.getTime());
	}, n * 1000);
	d.canceller = function () { try { clearTimeout(id) } catch (e) {} };
	return d;
};

Deferred.next = function (fun) {
	var d = new Deferred();
	var id = setTimeout(function () { clearTimeout(id); d.call() }, 0);
	if (fun) d.callback.ok = fun;
	d.canceller = function () { try { clearTimeout(id) } catch (e) {} };
	return d;
};

Deferred.call = function (f, args) {
	args = Array.prototype.slice.call(arguments);
	f    = args.shift();
	return Deferred.next(function () {
		return f.apply(this, args);
	});
};

Deferred.loop = function (n, fun) {
	var o = {
		begin : n.begin || 0,
		end   : (typeof n.end == "number") ? n.end : n - 1,
		step  : n.step  || 1,
		last  : false,
		prev  : null
	};
	var ret, step = o.step;
	return Deferred.next(function () {
		function _loop (i) {
			if (i <= o.end) {
				if ((i + step) > o.end) {
					o.last = true;
					o.step = o.end - i + 1;
				}
				o.prev = ret;
				ret = fun.call(this, i, o);
				if (ret instanceof Deferred) {
					return ret.next(function (r) {
						ret = r;
						return Deferred.call(_loop, i + step);
					});
				} else {
					return Deferred.call(_loop, i + step);
				}
			} else {
				return ret;
			}
		}
		return (o.begin <= o.end) ? Deferred.call(_loop, o.begin) : null;
	});
};

Deferred.register = function (name, fun) {
	this.prototype[name] = function () {
		return this.next(Deferred.wrap(fun).apply(null, arguments));
	};
};

Deferred.wrap = function (dfun) {
	return function () {
		var a = arguments;
		return function () {
			return dfun.apply(null, a);
		};
	};
};

Deferred.register("loop", Deferred.loop);
Deferred.register("wait", Deferred.wait);

Deferred.define = function (obj, list) {
	if (!list) list = ["parallel", "wait", "next", "call", "loop"];
	if (!obj)  obj  = (function () { return this })();
	for (var i = 0; i < list.length; i++) {
		var n = list[i];
		obj[n] = Deferred[n];
	}
	return Deferred;
};



function xhttp (opts) {
	var d = Deferred();
	if (opts.onload)  d = d.next(opts.onload);
	if (opts.onerror) d = d.error(opts.onerror);
	opts.onload = function (res) {
		d.call(res);
	};
	opts.onerror = function (res) {
		d.fail(res);
	};
	setTimeout(function () {
		GM_xmlhttpRequest(opts);
	}, 0);
	return d;
}
xhttp.get  = function (url)       { return xhttp({method:"get",  url:url}) };
xhttp.post = function (url, data) { return xhttp({method:"post", url:url, data:data, headers:{"Content-Type":"application/x-www-form-urlencoded"}}) };


function http (opts) {
	var d = Deferred();
	var req = new XMLHttpRequest();
	req.open(opts.method, opts.url, true);
	if (opts.headers) {
		for (var k in opts.headers) if (opts.headers.hasOwnProperty(k)) {
			req.setRequestHeader(k, opts.headers[k]);
		}
	}
	req.onreadystatechange = function () {
		if (req.readyState == 4) d.call(req);
	};
	req.send(opts.data || null);
	d.xhr = req;
	return d;
}
http.get   = function (url)       { return http({method:"get",  url:url}) };
http.post  = function (url, data) { return http({method:"post", url:url, data:data, headers:{"Content-Type":"application/x-www-form-urlencoded"}}) };
http.jsonp = function (url, params) {
	if (!params) params = {};

	var Global = (function () { return this })();
	var d = Deferred();
	var cbname = params["callback"];
	if (!cbname) do {
		cbname = "callback" + String(Math.random()).slice(2);
	} while (typeof(Global[cbname]) != "undefined");

	params["callback"] = cbname;

	url += (url.indexOf("?") == -1) ? "?" : "&";

	for (var name in params) if (params.hasOwnProperty(name)) {
		url = url + encodeURIComponent(name) + "=" + encodeURIComponent(params[name]) + "&";
	}

	var script = document.createElement('script');
	script.type    = "text/javascript";
	script.charset = "utf-8";
	script.src     = url;
	document.body.appendChild(script);

	Global[cbname] = function callback (data) {
		delete Global[cbname];
		document.body.removeChild(script);
		d.call(data);
	};
	return d;
};

Deferred.Deferred = Deferred;
Deferred.http     = http;
Deferred.xhttp    = xhttp;
return Deferred;
}// End of JSDeferred

// {{{
// }}}


})(this);

}))+"()";
