/* ronn.js version 0.1
 * Copyright : 2010 Jérémy Lal <kapouer@melix.org>
 * License : MIT
 */

var md = require(__dirname + '/ext/markdown');
var sys = require('sys');

/* exports Ronn class
 * usage :
 * var ronn = new Ronn(rofftext, "1.0", "my manual name", "2010-12-25");
 * ronn.roff();
 * ronn.html();
 * ronn.fragment();
 */

exports.Ronn = function(text, version, manual, date) {
	if (!manual) manual = "";
	if (!version) version = "";
	if (!date) date = new Date();
	else date = new Date(date + " GMT");

	var gHtml = md.toHTMLTree(text);

	this.roff = function() {
		return blockFilter("", gHtml, {parent:null, previous:null, position:null});
	};

	this.html = function() {
		return toHTML(gHtml);
	};

	this.fragment = function() {
		return toHTMLfragment(gHtml);
	};

	function blockFilter(out, node, context) {
		if (typeof node == "string") {
			if (!node.match(/^\s*$/m)) sys.debug("unexpected text: " + node);
			return out;
		}
		var tag = node.shift();	
		var attributes = null;
		if (node.length && typeof node[0] === "object" && !(node[0] instanceof Array)) {
			attributes = node.shift();
		}
		var fParent = context.parent;
		var fPrevious = context.previous;
		context.previous = null;
		context.parent = tag;
		switch (tag) {
			case "html":
				out = comment(out, "Generated with Ronnjs/v0.1");
				out = comment(out, "http://github.com/kapouer/ronnjs/");
				while (node.length) out = blockFilter(out, node.shift(), context);
			break;
			case "h1":
				var fTagline = node.shift();
				var fMatch = /([\w_.\[\]~+=@:-]+)\s*\((\d\w*)\)\s*-+\s*(.*)/.exec(fTagline);
				var fName, fSection;
				if (fMatch != null) {
					fName = fMatch[1];
					fSection = fMatch[2];
					fTagline = fMatch[3];
				} else {
					fMatch = /([\w_.\[\]~+=@:-]+)\s+-+\s+(.*)/.exec(fTagline);
					if (fMatch != null) {
						fName = fMatch[1];
						fTagline = fMatch[2];
					}
				}
				if (fMatch == null) {
					fName = "";
					fSection = "";
					fName = "";
				}
				out = macro(out, "TH", [
					quote(esc(fName.toUpperCase()))
					, quote(fSection)
					, quote(manDate(date))
					, quote(version)
					, quote(manual)
				]);
				out = macro(out, "SH", quote("NAME"));
				out += "\\fB" + fName + "\\fR";
				if (fTagline.length > 0) out += " \\-\\- " + esc(fTagline);
			break;
			case "h2":
				out = macro(out, "SH", quote(esc(toHTML(node.shift()))));
			break;
			case "h3":
				out = macro(out, "SS", quote(esc(toHTML(node.shift()))));
			break;
			case "hr":
				out = macro(out, "HR");
			break;
			case "p":
				if (fPrevious && fParent && (fParent == "dd" || fParent == "li"))
					out = macro(out, "IP");
				else if (fPrevious && !(fPrevious == "h1" || fPrevious == "h2" || fPrevious == "h3"))
					out = macro(out, "P");
				out = callInlineChildren(out, node, context);
			break;
			case "pre":
				var indent = (fPrevious == null || !(fPrevious == "h1" || fPrevious == "h2" || fPrevious == "h3"));
				if (indent) out = macro(out, "IP", [quote(""), 4]);
				out = macro(out, "nf");
				out = callInlineChildren(out, node, context);
				out = macro(out, "fi");
				if (indent) out = macro(out, "IP", [quote(""), 0]);
			break;
			case "dl":
				out = macro(out, "TP");
				while (node.length) out = blockFilter(out, node.shift(), context);
			break;
			case "dt":
				if (fPrevious != null) out = macro(out, "TP");
				out = callInlineChildren(out, node, context);
				out += "\n";
			break;
			case "dd":
				if (containsTag(node, {'p':true})) {
					while (node.length) out = blockFilter(out, node.shift(), context);
				} else {
					out = callInlineChildren(out, node, context);
				}
				out += "\n";
			break;
			case "ol":
			case "ul":
				context.position = 0;
				while (node.length) {
					out = blockFilter(out, node.shift(), context);
				}
				context.position = null;
				out = macro(out, "IP", [quote(""), 0]);
			break;
			case "li":
				if (fParent == "ol") {
					context.position += 1;
					out = macro(out, "IP", [quote(context.position), 4]);
				} else if (fParent == "ul") {
					out = macro(out, "IP", [quote("\\(bu"), 4]);
				}
				if (containsTag(node, {"p":true, "ol":true, "ul":true, "dl":true, "div":true})) {
					while (node.length) out = blockFilter(out, node.shift(), context);
				} else {
					out = callInlineChildren(out, node, context);
				}
				out += "\n";
			break;
			default:
				sys.debug("unrecognized block tag: " + tag);
			break;
		}
		context.parent = fParent;
		context.previous = tag;
		return out;
	}

	function callInlineChildren(out, node, context) {
		while (node.length) {
			var lChild = node.shift();
			if (node.length > 0) context.hasNext = true;
			else context.hasNext = false;
			out = inlineFilter(out, lChild, context);
		}
		return out;
	}

	function inlineFilter(out, node, context) {
		if (typeof node == "string") {
			if (context.previous && context.previous == "br") node = node.replace(/^\n+/gm, '');
			if (context.parent == "pre") {
				// do nothing
			} else if (context.previous == null && !context.hasNext) {
				node = node.replace(/\n+$/gm, '');
			} else {
				node = node.replace(/\n+$/gm, ' ');
			}
			out += esc(node);
			return out;
		}
		var tag = node.shift();	
		var attributes = null;
		if (node.length && typeof node[0] === "object" && !(node[0] instanceof Array)) {
			attributes = node.shift();
		}
		var fParent = context.parent;
		var fPrevious = context.previous;
		context.parent = tag;
		context.previous = null;
		switch(tag) {
			case "code":
				if (fParent == "pre") {
					out = callInlineChildren(out, node, context);
				} else {
					out += '\\fB';
					out = callInlineChildren(out, node, context);
					out += '\\fR';
				}
			break;
			case "b":
			case "strong":
			case "kbd":
			case "samp":
				out += '\\fB';
				out = callInlineChildren(out, node, context);
				out += '\\fR';
			break;
			case "var":
			case "em":
			case "i":
			case "u":
				out += '\\fI';
				out = callInlineChildren(out, node, context);
				out += '\\fR';
			break;
			case "br":
				out = macro(out, "br");
			break;
			case "a":
				var fStr = node[0];
				var fHref = attributes['href'];
				if (fHref == fStr || decodeURI(fHref) == "mailto:" + decodeURI(fStr)) {
					out += '\\fI';
					out = callInlineChildren(out, node, context);
					out += '\\fR';
				} else {
					out = callInlineChildren(out, node, context);
					out += " ";
					out += '\\fI';
					out += esc(fHref);
					out += '\\fR';
				}
			break;
			default:
				sys.debug("unrecognized inline tag: " + tag);
			break;
		}
		context.parent = fParent;
		context.previous = tag;
		return out;
	}

	function containsTag(node, tags) {
		// browse ml tree searching for tags (hash {tag : true, ...})
		if (typeof node == "string") return false;
		var jml = node.slice(0);
		if (jml.length == 0) return false;
		else while (jml.length && jml[0] instanceof Array) {
			if (containsTag(jml.shift(), tags)) return true;
		}
		var tag = jml.shift();
		if (tags[tag] === true) return true;
		if (jml.length && typeof jml[0] === "object" && !(jml[0] instanceof Array)) {
			// skip attributes
			jml.shift();
		}
		// children
		if (jml.length) {
			if (containsTag(jml.shift(), tags)) return true;
		}
		// siblings
		if (jml.length) return containsTag(jml, tags);
	}

	function toHTML(node) {
		// problème ici : les & sont remplacés par des &amp;
		return md.renderJsonML(node, {root:true, xhtml:true});
	}

	function toHTMLfragment(node) {
		return md.renderJsonML(node, {xhtml:true});
	}

	function comment(out, str) {
		return writeln(out, '.\\" ' + str);
	}

	function quote(str) {
		return '"' + str + '"';
	}

	function esc(str) {
		return str
			.replace(/\\/gm, "\\\\")
			.replace(/-/gm, "\\-")
			.replace(/^\./gm, "\\|.")
			.replace(/\./gm, "\\.")
			.replace(/'/gm, "\\'")
			;
	}

	function writeln(out, str) {
		if (out.length && out[out.length - 1] != "\n") out += "\n";
		out += str + "\n";
		return out;
	}

	function macro(out, name, list) {
		var fText = ".\n." + name;
		if (list != null) {
			if (typeof list == "string") {
				fText += ' ' + list;
			} else {
				for (var i=0, len=list.length; i < len; i++) {
					var item = list[i];
					if (item == null) continue;
					fText += ' ' + item;
				}
			}
		}
		return writeln(out, fText);
	}

	function manDate(pDate) {
		var fMonth = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][pDate.getMonth()];
		return fMonth + " " + pDate.getFullYear();
	}
};
