// TODO:
//  look at http://prototype.conio.net/

//
// Configuration
//

//var QUERY_BASE="http://localhost/~enrico/cgi-bin";
var QUERY_BASE="http://debtags.alioth.debian.org/cgi-bin";

var hasCookies = false;

//
// Debtags infrastructure
//

Tagset = function() {
	this._items = {};
	this._size = 0;
	for (var i = 0; i < arguments.length; ++i)
		this.add(arguments[i]);
};
Tagset.prototype = {
	clear: function() {
		this._items = new Object();
		this._size = 0;
	},
  has: function(name) {
		return this._items.hasOwnProperty(name);
	},
	hasRE: function(re) {
		var res = false;
		this.each(function(t) { if(re.exec(t)) res = true; });
		return res;
	},
	add: function(name) {
		if (this.has(name))
			return false;
		this._items[name] = true;
		++this._size;
		return true;
	},
	addAll: function(names) {
		for (i in names)
			this.add(names[i]);
	},
	del: function(name) {
		if (this.has(name))
		{
			delete this._items[name];
			--this._size;
			return true;
		}
		return false;
	},
	size: function() {
		return this._size;
	},
	empty: function() {
		return this._size == 0;
	},
	addFirstTags: function(tags, coll) {
		var lastCard = 0;
		function score(x) { return ((x-15)*(x-15))/x; }

		for (i in tags) {
			var tag = tags[i][0];
			var card = parseInt(tags[i][1]);
			if (i == 0 || score(lastCard) > score(card)) {
				this.add(tag);
				lastCard = card;
			} else
				break;
		}

		// Return always at least the first tag
		if (this.empty() && tags.length > 0)
			this.add(tags[0][0]);
	},
	each: function(fun) {
		for (var i in this._items)
			fun(i);
	},
	merge: function(tset) {
		var tmp=this;
		tset.each(function(i){tmp.add(i)});
	},
	subtract: function(tset) {
		var tmp=this;
		tset.each(function(i){tmp.del(i)});
	},
	contains: function(tset) {
		for (var i in tset._items)
			if (!this.has(i))
				return false;
		return true;
	},
	intersects: function(tset) {
		for (var i in tset._items)
			if (this.has(i))
				return true;
		return false;
	},
	toString: function() {
		var str = "";
		this.each(function(i){if(str == "") str+=i; else str+=', '+i;});
		return str;
	},
	copy: function() {
		var res = new Tagset();
		this.each(function(i){res.add(i);});
		return res;
	},
	mkpatch: function(ts) {
		var added = ts.copy();
		added.subtract(this);
		var removed = this.copy();
		removed.subtract(ts);
		var res = new Tagset();
		added.each(function(tag){res.add("+"+tag);});
		removed.each(function(tag){res.add("-"+tag);});
		return res;
	}
	/*
	filtervoc: function(voc) {
		var voc1 = new Vocabulary();
		// TODO: return a vocabulary with only those tags in voc that are also in
		// this tagset
	}
	*/
};

Collection = function() {
	this.db = {};
	this.pcount = 0;
};
Collection.prototype = {
  clear: function() {
		this.db = {};
		this.pcount = 0;
	},
	decode: function(lines) {
		var packageDataRegexp = new RegExp("^(\\S+) (\\S*) (.+)$");

		for (var line in lines)
		{
			var fields = packageDataRegexp.exec(lines[line]);
			if (!fields || fields.length < 4)
				continue;
			tset = new Tagset();
			tset.addAll(fields[2].split(","));
			this.addPackage(fields[1], tset);
		}
	},
	addPackage: function(pkg, tagset) {
		if (this.db.hasOwnProperty(pkg))
			this.db[pkg].merge(tagset);
		else
		{
			this.db[pkg] = tagset;
			++this.pcount;
		}
	},
	hasPackage: function(pkg) {
		return this.db.hasOwnProperty(pkg);
	},
	tagsOfPackage: function(pkg) {
		if (this.db.hasOwnProperty(pkg))
			return this.db[pkg];
		else
			return new Tagset();
	},
	iterPackages: function(fun) {
		for (var i in this.db)
			fun(i, this.db[i]);
	},
	packageCount: function() {
		return this.pcount;
	},
	merge: function(coll) {
		var tmp = this;
		coll.iterPackages(function(p,t){tmp.addPackage(p,t);});
	}
};

Packages = function() {
	this.db = {};
};
Packages.prototype = {
	decode: function(lines) {
		var packageDataRegexp = new RegExp("^(\\S+) (\\S*) (.+)$");

		for (var line in lines)
		{
			var fields = packageDataRegexp.exec(lines[line]);
			if (!fields || fields.length < 4)
				continue;
			this.add(fields[1], fields[3]);
		}
	},
	add: function(name, sdesc) {
		this.db[name] = sdesc;
	},
	get: function(name) {
		if (this.db.hasOwnProperty(name))
			return this.db[name];
		else
			return null;
	},
	each: function(fun) {
		for (var i in this.db)
			fun(i, this.db[i]);
	},
	merge: function(pkgs) {
		var tmp = this;
		pkgs.each(function(p,d){tmp.add(p,d);});
	}
};

Vocabulary = function(fdata, tdata) {
	this.facs = fdata;
	this.tags = tdata;
	this.ftags = {};
	for (var i in this.tags) {
		var tmp = i.split("::");
		if (! this.ftags.hasOwnProperty(tmp[0]))
			this.ftags[tmp[0]] = new Tagset();
		this.ftags[tmp[0]].add(i);
	}
};
Vocabulary.prototype = {
	// Return an array [shortdesc, longdesc]
	facData: function(fac) {
		return this.facs[fac];
	},
	// Return an array [shortdesc, longdesc]
	tagData: function(tag) {
		return this.tags[tag];
	},
	tagsOfFacet: function(fac) {
		return this.ftags[fac];
	},
	eachFacet: function(fun) {
		for (var i in this.facs) {
			fun(i, this.facs[i][0], this.facs[i][1]);
		}
	},
	eachTag: function(fun) {
		for (var i in this.tags) {
			fun(i, this.tags[i][0], this.tags[i][1]);
		}
	},
	eachTagOfFacet: function(fac, fun) {
		var ts = this.tagsOfFacet(fac);
		var tmp = this;
		if (ts) ts.each(function(t){
			fun(t, tmp.tags[t][0], tmp.tags[t][1]);
		});
	}
};


//
// Communication with the server
//

// Create an XMLHttpRequest object
function getXMLHttpRequest()
{
// From http://jibbering.com/2002/4/httprequest.html
var req = false;
/*@cc_on @*/
/*@if (@_jscript_version >= 5)
// JScript gives us Conditional compilation, we can cope with old IE versions.
// and security blocked creation of the objects.
try {
req = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
try {
	req = new ActiveXObject("Microsoft.XMLHTTP");
} catch (E) {
	req = false;
}
}
@end @*/
if (!req && typeof XMLHttpRequest != 'undefined') {
	req = new XMLHttpRequest();
}
return req;
}

function runRequest(url, progressfunc, finalfunc, async)
{
	// Opera doesn't seem to support async queries
	if (window.opera) async = false;
	try {
		var req = getXMLHttpRequest();
		req.open("GET", url, async);
		if (async)
		{
			req.onreadystatechange = function() {
				switch (req.readyState)
				{
					case 4:
						if (req.status != 200) {
							alert("Getting " + url + " failed with status " + req.statusText);
							finalfunc(new Array());
						}
						else
							finalfunc(req.responseText.split("\n"));
						break;
					default:
						progressfunc(req.responseText.split("\n"));
						break;
				}
			};
		}
		req.send(null);
		if (!async)
			finalfunc(req.responseText.split("\n"));
	} catch (e) {
		alert("Cannot get data from " + url + ": " + e);
		finalfunc(new Array());
	}
}

function runPostRequest(url, content, progressfunc, finalfunc, async)
{
	// Opera doesn't seem to support async queries
	if (window.opera) async = false;
	try {
		var req = getXMLHttpRequest();
		req.open("POST", url, async);
		req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
		if (async)
		{
			req.onreadystatechange = function() {
				switch (req.readyState)
				{
					case 4:
						if (req.status != 200) {
							alert("Posting to " + url + " failed with status " + req.statusText);
							finalfunc(new Array());
						}
						else
							finalfunc(req.responseText.split("\n"));
						break;
					default:
						progressfunc(req.responseText.split("\n"));
						break;
				}
			};
		}
		req.send(content);
		if (!async)
			finalfunc(req.responseText.split("\n"));
	} catch (e) {
		alert("Cannot post data to " + url + ": " + e);
		finalfunc(new Array());
	}
}

function fireFTSQuery(str, progressFunc, finalFunc, async)
{
// Build the new search base
var keys = str.split(" ");
for (i in keys)
	keys[i] = escape(keys[i]);

runRequest(QUERY_BASE + "/fts/"+keys.join('/'),
	function(lines) {
		var coll = new Collection();
		var pkgs = new Packages();
		coll.decode(lines);
		pkgs.decode(lines);
		progressFunc(coll, pkgs);
	},
	function(lines) {
		var coll = new Collection();
		var pkgs = new Packages();
		coll.decode(lines);
		pkgs.decode(lines);
		finalFunc(coll, pkgs);
	},
	async
);
}

function fireSTAGSQuery(str, progressFunc, finalFunc, async)
{
// Build the new search base
var keys = str.split(" ");
for (i in keys)
	keys[i] = escape(keys[i]);

runRequest(QUERY_BASE + "/stags/"+keys.join('/'),
	function(lines) {
		var ts = new Array();
		for (i in lines)
		{
			var fields = lines[i].split(" ");
			ts.push(fields);
		}
		progressFunc(ts);
	},
	function(lines) {
		var ts = new Array();
		for (i in lines)
		{
			var fields = lines[i].split(" ");
			ts.push(fields);
		}
		finalFunc(ts);
	},
	async
);
}

function fireSUGGQuery(pkg, progressFunc, finalFunc, async)
{
// Build the new search base
runRequest(QUERY_BASE + "/sugg/"+pkg,
	function(lines) {
		var ts = new Array();
		for (i in lines)
		{
			var fields = lines[i].split(" ");
			ts.push(fields);
		}
		progressFunc(ts);
	},
	function(lines) {
		var ts = new Array();
		for (i in lines)
		{
			var fields = lines[i].split(" ");
			ts.push(fields);
		}
		finalFunc(ts);
	},
	async
);
}

function fireUNTQuery(count, progressFunc, finalFunc, async)
{
	runRequest(QUERY_BASE + "/unt/"+count,
		function(lines) {
			var coll = new Collection();
			var pkgs = new Packages();
			coll.decode(lines);
			pkgs.decode(lines);
			progressFunc(coll, pkgs);
		},
		function(lines) {
			var coll = new Collection();
			var pkgs = new Packages();
			coll.decode(lines);
			pkgs.decode(lines);
			finalFunc(coll, pkgs);
		},
		async
	);
}

function fireUntExtraQuery(name, progressFunc, finalFunc, async)
{
	runRequest(QUERY_BASE + "/" + name,
		function(lines) {
			var coll = new Collection();
			var pkgs = new Packages();
			coll.decode(lines);
			pkgs.decode(lines);
			progressFunc(coll, pkgs);
		},
		function(lines) {
			var coll = new Collection();
			var pkgs = new Packages();
			coll.decode(lines);
			pkgs.decode(lines);
			finalFunc(coll, pkgs);
		},
		async
	);
}

// Query the package list for a maintainer
function fireMaintQuery(email, progressFunc, finalFunc, async)
{
	runRequest(QUERY_BASE + "/maint/" + email,
		function(lines) {
			var coll = new Collection();
			var pkgs = new Packages();
			coll.decode(lines);
			pkgs.decode(lines);
			progressFunc(coll, pkgs);
		},
		function(lines) {
			var coll = new Collection();
			var pkgs = new Packages();
			coll.decode(lines);
			pkgs.decode(lines);
			finalFunc(coll, pkgs);
		},
		async
	);
}

function firePKGSQuery(ts, progressFunc, finalFunc, async)
{
var tags = new Array();
ts.each(function(i){tags.push(escape(i))});
runRequest(QUERY_BASE + "/pkgs/"+tags.join('/'),
	function(lines) {
		var coll = new Collection();
		var pkgs = new Packages();
		coll.decode(lines);
		pkgs.decode(lines);
		progressFunc(coll, pkgs);
	},
	function(lines) {
		var coll = new Collection();
		var pkgs = new Packages();
		coll.decode(lines);
		pkgs.decode(lines);
		finalFunc(coll, pkgs);
	},
	async
);
}

function firePKGQuery(pkg, progressFunc, finalFunc, async)
{
	var packageDataRegexp = new RegExp("^(\\S+) (\\S*) (.+)$");
	var runDecoded = function(fun, lines){
	if (lines.length == 0)
			return fun(null, null, null);

		var fields = packageDataRegexp.exec(lines.shift());
		if (!fields || fields.length < 4)
			return fun(null, null, null);

		var ts = new Tagset();
		ts.addAll(fields[2].split(","));

		// Format the long description into HTML
		for (i in lines)
		{
			// Trim leading and trailing spaces
			lines[i] = lines[i].replace(/^[ \t]/, "");
			lines[i] = lines[i].replace(/[ \t]$/, "");
			// Format paragraph breaks
			if (lines[i] == "") lines[i] = "</p><p>";
			lines[i] = lines[i].replace(/^[*+-][ \t]+/, "<li>");
		}
		lines = "<p>"+lines.join("\n")+"</p>";
		return fun(ts, fields[3], lines);
	};
	runRequest(QUERY_BASE + "/pkg/"+pkg,
		function(lines) {
			runDecoded(progressFunc, lines);
		},
		function(lines) {
			runDecoded(finalFunc, lines);
		},
		async
	);
}

function decodeSimpleRFC822(lines)
{
	var lineRE = new RegExp("^([^:]+): (.+)$");
	var res = new Object();
	for (var i in lines)
	{
		var fields = lineRE.exec(lines[i]);
		if (fields)
		{
			var name = fields[1].toLowerCase();
			res[name] = fields[2];
		}
	}
	return res;
}

function fireQSTATSQuery(progressFunc, finalFunc, async)
{
	runRequest(QUERY_BASE + "/qstats",
		function(lines) {
			progressFunc(decodeSimpleRFC822(lines));
		},
		function(lines) {
			finalFunc(decodeSimpleRFC822(lines));
		},
		async
	);
}


function fireSubmitPatch(patch, progressFunc, finalFunc, async, tag)
{
	var content = "patch="+encodeURIComponent(patch);
	if (tag) content += "&tag="+encodeURIComponent(tag);
	runPostRequest(QUERY_BASE + "/patch", content, progressFunc, finalFunc, async);
}


//
// Web application helpers
//

// Cookies

// From http://www.quirksmode.org/js/cookies.html
function createCookie(name,value,days)
{
	if (days)
	{
		var date = new Date();
		date.setTime(date.getTime()+(days*24*60*60*1000));
		var expires = "; expires="+date.toGMTString();
	}
	else
		var expires = "";
	document.cookie = name+"="+value+expires+"; path=/";
}

// From http://www.quirksmode.org/js/cookies.html
function readCookie(name)
{
	var nameEQ = name + "=";
	var ca = document.cookie.split(';');
	for(var i=0;i < ca.length;i++) {
		var c = ca[i];
		while (c.charAt(0)==' ') c = c.substring(1,c.length);
		if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
	}
	return null;
}

// From http://www.quirksmode.org/js/cookies.html
function eraseCookie(name) {
	createCookie(name,"",-1);
}

// Retrieve or create a session identification tags
function sessionTag()
{
	var oldTag = readCookie("session");
	if (oldTag == null)
		oldTag = new UUID();

	// Create or refresh the cookie
	createCookie("session", oldTag, 14);

	hasCookies = (readCookie("session") == oldTag);

	return oldTag;
}


// Rendering helpers

function render(id, content)
{
	var node = document.getElementById(id);
	node.innerHTML = content;
}

function setVisible(id, visible)
{
	var node = document.getElementById(id);
	if (visible)
		node.style.display = "block";
	else
		node.style.display = "none";
}
function setAttr(id, name, val)
{
	var node = document.getElementById(id);
	node.style[name] = val;
}
function eachElementByClass(type, cl, fun)
{
	var elements;
	if (type == '*')
		// '*' not supported by IE/Win 5.5 and below
		elements = (document.all) ? document.all : document.getElementsByTagName('*');
	else
		elements = document.getElementsByTagName(type);

	for (var i = 0; i < elements.length; i++)
	{
		var node = elements.item(i);
		if (!node.attributes) continue;
		for (var j = 0; j < node.attributes.length; j++)
			if (node.attributes.item(j).nodeName == 'class')
				if(node.attributes.item(j).nodeValue == cl)
					fun(node);
	}
}


// Reusable application logic

function getQuery()
{
	var query = window.location.search.substring(1);
	query = query.split('&');
	var res = {};
	for (i in query)
	{
		var q = query[i].split('=');
		res[unescape(q[0])] = unescape(q[1]);
	}
	return res;
}

function commentTagset(pkg, tags)
{
	var res = new Array();
	if (!hasCookies)
		res.push("Please enable cookies for this page: it makes your contributions easier to process.  For questions, please <a href='mailto:debtags-devel@lists.alioth.debian.org'>mail the Debtags mailing list</a>.");
	if (tags.hasRE(/^special::not-yet-tagged/))
		res.push("The <i>not-yet-tagged</i> tags are still present.");
	if (!tags.hasRE(/^role::/))
		res.push("A <i>role::*</i> tag is still missing.");
	if (tags.hasRE(/^(interface::(3d|x11)|x11::application)$/) && !tags.hasRE(/^uitoolkit::/))
		res.push("An <i>uitoolkit::*</i> tag seems to be missing.");
	if (tags.hasRE(/^role::(program|devel-lib|plugin|shared-lib|source)$/) && ! tags.hasRE(/^implemented-in::/))
		res.push("An <i>implemented-in::*</i> tag seems to be missing.");
	if (tags.hasRE(/^(role::devel-lib|devel::library)$/) && ! tags.hasRE(/^devel::lang:/))
		res.push("A <i>devel::lang:*</i> tag seems to be missing.");
	if (tags.hasRE(/^role::devel-lib$/) && ! tags.hasRE(/^devel::library/))
		res.push("A <i>devel::library</i> tag seems to be missing.");
	// TODO: this could be implemented by auto-adding the tags and at that point
	// the inferrer will pick it up
	if (tags.hasRE(/^devel::library$/) && ! tags.hasRE(/^role::devel-lib$/))
		res.push("A <i>role::devel-lib</i> tag seems to be missing.");
	if (tags.hasRE(/^use::gameplaying$/) && ! tags.hasRE(/^game::/))
		res.push("A <i>game::*</i> tag seems to be missing.");
	return res;
}

tagsetExtraWarnings = function(pkg, tags) {
	return new Array();
}

// vim:set ts=2 sw=2:
