// ==UserScript==
// @name        jAutoPagerize
// @description Just Another AutoPagerize
// @namespace   http://lowreal.net/
// @include     http://*
// ==/UserScript==
// This script uses JavaScript 1.7, 1.8 functions:
//    Array.prototype.{filter,forEach,map,reduce} etc...
//
// $Date$
//
// Latest::  http://svn.coderepos.org/share/lang/javascript/userscripts/jautopagerize.user.js

// define export object;
AutoPagerize = {};
AutoPagerize.VERSION = "jautopagerize $Rev$";
AutoPagerize.Config  =
{ site_info_urls : [ 'http://swdyh.infogami.com/autopagerize'
                   , 'http://userjs.oh.land.to/pagerization/convert.php?file=siteinfo.v5'
                   ]
, site_info      : [ {}
                   /* template
                   , { url          : ''
                     , nextLink     : ''
                     , insertBefore : ''
                     , pageElement  : ''
                   }
                   */
                   ]
, color          : { on         : '#22a06d'
                   , off        : '#cccccc'
                   , loading    : '#00587f'
                   , terminated : '#000000'
                   , error      : '#990000'
                   }
, cache_expire   : 24 * 60 * 60 * 1000
, remain_height  : 400
, autostart      : true
};

/*
 * filter APIs
 */
AutoPagerize.filters = [];
AutoPagerize.addFilter = function (fun) {
	AutoPagerize.filters.push(fun);
};

window.AutoPagerize = AutoPagerize;

(function () {
	if (window != window.parent) return;
	BeCompatible();

	/*
	 * define utilities
	 */

	// Async is similar to Mochikit Deferred.
	function Async () { this.init.apply(this, arguments) }
	Async.prototype = {
		init : function () {
			this.chain = { ok: [], ng: [] };
		},

		ready : function (value) {
			return this.call("ok", value);
		},

		error : function (value) {
			return this.call("ng", value);
		},

		call : function (okng, value) {
			if (typeof value == "function") {
				this.chain[okng].push(value);
			} else { try {
				var c = this.chain[okng];
				for (var i = 0; i < c.length; i++) {
					c[i](value);
				}
			} catch (e) { this.call("ng", e) } }
			return this;
		}
	};

	function AsyncList (asyncs) {
		var ret = new Async();
		var values = [];
		asyncs.forEach(function (a, i) {
			a.ready(function (v) {
				asyncs.pop();
				values[i] = v;
				if (asyncs.length == 0) {
					ret.ready(values);
				}
			});
			a.error(function (v) {
				ret.error(v);
			});
			values.push(null);
		});
		return ret;
	}

	function Resource (uri, convertfun) {
		var a = new Async();
		if (!convertfun) convertfun = function (i) { return i };
		GM_xmlhttpRequest({
			method  : "GET",
			url     : uri,
			onload  : function (req) { try {
				var res = convertfun(req.responseText);
				a.ready(res);
			} catch (e) { a.error(e) } },
			onerror : function (e) {
				a.error(e);
			}
		});
		return a;
	}

	function HTMLResource (uri) {
		function createDocumentFromString (s) {
			s = String(s);
			s = s.replace(/<script[^>]+>([^\s]|\s)*?<\/script>/g, "");
			s = s.replace(/<\/?(i?frame|html|script|object)[^<>]+>/g, "");
			if (document.implementation.createHTMLDocument) {
				var d = document.implementation.createHTMLDocument("");
				var r = d.createRange();
				while (d.documentElement.firstChild) d.documentElement.removeChild(d.documentElement.firstChild);
				r.selectNodeContents(d.documentElement);
				d.documentElement.appendChild(r.createContextualFragment(s));

//				d.documentElement.innerHTML = s;
//				alert(d.documentElement.innerHTML);
				return d;
			} else {
				log("fallback: use div to parse HTML");
				$X.forceRelative = true;
				var d = document.createElement("div");
				d.innerHTML = s;
				return d;
			}
		}
		return Resource(uri, createDocumentFromString);
	}

	function CachedResource (uri, convertfun, expire) {
		var a   = new Async();
		var key = uri;
		var v   = eval(GM_getValue(key)) || ({});
		a.clear = function () {
			GM_setValue(key, "");
			return this;
		};
		if (v.time && v.time > (new Date).getTime() - expire) {
			log("Cache Hitted: " + key);
			setTimeout(function () { a.ready(v.body); }, 10);
		} else {
			log("Cache expired; getting... " + key);
			GM_xmlhttpRequest({
				method  : "GET",
				url     : uri,
				onload  : function (req) { try {
					var res = convertfun(req.responseText);
					GM_setValue(key, {time:(new Date).getTime(), body:res}.toSource());
					log("Cached: " + key);
					a.ready(res);
				} catch (e) { a.error(e) } },
				onerror : function (e) {
					a.error("HTTPError:"+e);
				}
			});
		}
		return a;
	};

	/*
	 * jAutoPagerize
	 */

	AutoPagerize.parseSiteInfo = function (html) {
		var d = h(html);

		var siteinfo = [];
		$X(".//*[@class='autopagerize_data']", d).forEach(function (e) {
			// using replace as scan and folding key/value to i
			var i = {}; e.value.replace(/^\s*([^:\s]+):\s*(.*)$/gm, function (m, key, value) {
				i[key] = value;
			});
			siteinfo.push(i);
		});
		return siteinfo;
	};

	AutoPagerize.onscroll = function (e) {
		var height = document.documentElement.scrollHeight || document.body.scrollHeight;
		var remain = height - window.innerHeight - window.scrollY;
		if (AutoPagerize.enable && (remain < AutoPagerize.Config.remain_height)) {
			AutoPagerize.loadNext();
		}
	};

	AutoPagerize.toggle = function () {
		AutoPagerize.enable = !AutoPagerize.enable;
		AutoPagerize.updateStatus();
		return AutoPagerize.enable;
	};

	AutoPagerize.loadNext = function () {
		if ( AutoPagerize._loading) return;
		if (!AutoPagerize._nextURI) {
			AutoPagerize._terminate = true;
			AutoPagerize.updateStatus();
			return;
		}

		AutoPagerize._loading = true;
		HTMLResource(AutoPagerize._nextURI).ready(function (r) {
			AutoPagerize.filters.forEach(function (f) { f(r) });
			setTimeout(function () { try {
				// in Safari 3 & GreaseKit. ib is often undefined
				// and freeze as refering this...
				var ib  = AutoPagerize._insertBefore;
				var pib = ib.parentNode;
				pib.insertBefore(h("<hr /><p>AutoPagerized: <a href='%s'>%s</a></p>".replace(/%s/g, AutoPagerize._nextURI)), ib);
				$X(AutoPagerize._pageinfo.pageElement, r).forEach(function (i) {
					log(String(i));
					i = document.importNode(i, true);
					pib.insertBefore(i, ib);
				});

				AutoPagerize._nextURI = ($X(AutoPagerize._pageinfo.nextLink, r)[0] || {}).href;
				AutoPagerize._loading = false;
				AutoPagerize.updateStatus();
			} catch (e) { AutoPagerize.errorHandler(e) } }, 10);
		}).error(AutoPagerize.errorHandler);
		AutoPagerize.updateStatus();
	};

	AutoPagerize.updateStatus = function () {
		var i = AutoPagerize.icon;
		var s = i.style;
		s.fontSize   = "0.7em";
		s.position   = "fixed";
		s.top        = "0";
		s.right      = "0";
		s.margin     = "0.3em";
		s.padding    = "0 1em";
		s.color      = "#fff";
		s.zIndex     = "9999";
		s.fontWeight = "bold";
		s.lineHeight = "1.33";

		if (AutoPagerize._error) {
			i.innerHTML  = AutoPagerize._error;
			s.background = AutoPagerize.Config.color["error"];
		} else
		if (AutoPagerize._terminate) {
			i.innerHTML  = ".";
			s.background = AutoPagerize.Config.color["terminated"];
		} else
		if (AutoPagerize._loading) {
			i.innerHTML  = "loading...";
			s.background = AutoPagerize.Config.color["loading"];
		} else {
			i.innerHTML  = ".";
			s.background = AutoPagerize.Config.color[AutoPagerize.enable ? "on" : "off"];
		}
	};


	AutoPagerize.init = function () {
		// init/define all state variables
		AutoPagerize.enable        = AutoPagerize.Config.autostart;
		AutoPagerize.icon          = h("<div id='autopagerize_icon'/>").firstChild;
		AutoPagerize._loading      = false;
		AutoPagerize._terminate    = false;
		AutoPagerize._error        = null;
		AutoPagerize._pageinfo     = [];
		AutoPagerize._nextURI      = null;
		AutoPagerize._insertBefore = null;

		// get siteinfo
		AsyncList(AutoPagerize.Config.site_info_urls.map(function (url) {
			var cr = CachedResource(url, AutoPagerize.parseSiteInfo, AutoPagerize.Config.cache_expire);
			if (location.href == url) cr.clear();
			return cr;
		})).ready(function (r) {
			r = AutoPagerize.Config.site_info.concat(r.reduce(function (r, i) { return r.concat(i) }));
			var info = r.filter(function (i) {
				if (!i.url)          return false;
				if (!i.nextLink)     return false;
				if (!i.pageElement)  return false;
				if (!i.insertBefore) return false;
				return location.href.match(i.url) && !!$X(i.insertBefore)[0];
			});
			if (!info.length) return;

			AutoPagerize._pageinfo     = info[0];
			AutoPagerize._nextURI      = ($X(AutoPagerize._pageinfo.nextLink)[0] || {}).href;
			AutoPagerize._insertBefore = $X(AutoPagerize._pageinfo.insertBefore)[0];
			document.body.appendChild(AutoPagerize.icon);

			window.addEventListener("scroll",   AutoPagerize.onscroll, false);
			window.addEventListener("dblclick", AutoPagerize.toggle,   false);

			AutoPagerize.updateStatus();
		}).error(AutoPagerize.errorHandler);
	};

	AutoPagerize.errorHandler = function (e) {
		AutoPagerize._error = String(e);
		AutoPagerize.updateStatus();
		alert(e.toSource());
	};

	// Set error handler to all functions
	for (var i in AutoPagerize) {
		if (AutoPagerize.hasOwnProperty(i) &&
		    typeof AutoPagerize[i] == "function") {
			AutoPagerize[i] = (function (f) {
				return function () { try {
					return f.apply(AutoPagerize, arguments);
				} catch (e) { AutoPagerize.errorHandler(e) } };
			})(AutoPagerize[i]);
		}
	}

	// Run!
	window.addEventListener("load", function () {
		AutoPagerize.init();
	}, false);


	function BeCompatible () {
		// Memo::
		// Safari 3 has JS 1.6 functions but 1.8 and toSource.

		if (!Array.prototype.reduce) {
			Array.prototype.reduce = function(fun /*, initial*/) {
				var len = this.length;
				if (typeof fun != "function") throw new TypeError();

				// no value to return if no initial value and an empty array
				if (len == 0 && arguments.length == 1) throw new TypeError();

				var i = 0;
				if (arguments.length >= 2) {
					var rv = arguments[1];
				} else {
					do {
						if (i in this) {
							rv = this[i++];
							break;
						}

						// if array contains no values, no initial value to return
						if (++i >= len) throw new TypeError();
					} while (true);
				}

				for (; i < len; i++) {
					if (i in this) rv = fun.call(null, rv, this[i], i, this);
				}

				return rv;
			};
		}

		if (!Object.prototype.toSource) {
			Object.prototype.toSource = function () {
				var props = [];
				for (var key in this) {
					if (this.hasOwnProperty(key)) {
						var v;
						switch (typeof this[key]) {
							case "undefined": v = "undefined"; break;
							case "null"     : v = "null"; break;
							default         : v = this[key].toSource();
						}
						props.push(key.toSource() + ":" + v);
					}
				}
				return "({" + props.join(",") + "})";;
			};

			String.prototype.toSource = function () {
				return '"' + this.replace(/"/g, '\\"') + '"';
			};

			Array.prototype.toSource = function () {
				return "[" + this.map(function (i) { return i.toSource() }).join(",") + "]";
			};

			Date.prototype.toSource = function () {
				return "(new Date(%d))".replace(/%d/, this.getTime());
			};

			Number.prototype.toSource = function () {
				return String(this);
			};

			RegExp.prototype.toSource = function () {
				return String(this);
			};

			Boolean.prototype.toSource = function () {
				return String(this);
			};
		}

		// Firefox doesn't have createHTMLDocument
		if (!document.implementation.createHTMLDocument &&
		    navigator.userAgent.match(/Firefox/)) {
			var x = new XSLTProcessor();
			x.importStylesheet((new DOMParser).parseFromString([
				"<stylesheet version='1.0' xmlns='http://www.w3.org/1999/XSL/Transform'>",
				"<output method='html'/>",
				"</stylesheet>"].join("\n"), "application/xml"));
			return x.transformToDocument((new DOMParser).parseFromString("<nice-boat/>", "application/xml"));
		}

//		[
//			{},
//			{date:new Date,body:[{a:1,b:2}]}
//		].forEach(function (i) {
//			alert(i.toSource());
//		});
//
//		throw "";
	}

	/*
	 * template functions
	 */

	function log (m) {
//		var c = unsafeWindow.console;
//		if (c) c.log.apply(c, arguments);
		var o = Array.prototype.concat.apply([], arguments);
		if (window.console) {
			window.console.log(o.join(", "));
		} else {
			location.href = "javascript:(function () { if (window.console) console.log.apply(console.log, "+o.toSource()+") })();";
		}
	}

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

	// extend version of $X
	// $X(exp);
	// $X(exp, context);
	// $X(exp, type);
	// $X(exp, context, type);
	function $X (exp, context, type /* want type */) {
		if (arguments.callee.forceRelative) {
			exp = exp.replace(/id\((?:"([^"]+)"|'([^']+)')\)/g, function (_, v1, v2) {
				return '//*[@id="' + (v1 || v2) + '"]';
			});
			if (exp.indexOf("(//") == 0)
				exp = "(.//" + exp.substring(3);
			else
				exp = ((exp[0] == "/") ? "." : "./") + exp;
		}
		log("xpath:" + exp);

		if (typeof context == "function") {
			type    = context;
			context = null;
		}
		if (!context) context = document;
		var exp = (context.ownerDocument || context).createExpression(exp, function (prefix) {
			var o = document.createNSResolver(context)(prefix);
			if (o) return o;
			return (document.contentType == "application/xhtml+xml") ? "http://www.w3.org/1999/xhtml" : "";
		});

		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; i++) {
					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."));
		}
	}

//	unsafeWindow.$X = $X;

})();

