'use strict';


var path = require('path'),
	parser = require('./parser'),
	runtime = require('./runtime'),
	bladeutil = require('./util'),
	doctypes = require('./doctypes'),
	selfClosingTags = require('./self-closing-tags'),
	filters = require('./filters');

module.exports = Compiler;

function Compiler(string, opts) {
	this.string = string;
	this.options = opts || {};
	//Copy all this crap into compiler options object
	var copy = {
		"doctypes": doctypes,
		"filters": filters
	};
	//Merge doctypes, filters, and selfClosingTags with those passed into `options`
	for(var i in copy)
	{
		this.options[i] = this.options[i] || {};
		for(var j in copy[i])
			if(this.options[i][j] == null)
				this.options[i][j] = copy[i][j];
	}
	//Self-closing tags
	this.options.selfClosingTags = this.options.selfClosingTags || selfClosingTags;
	//Store special options
	if(typeof this.options.filename == "string")
	{
		this.options.filename = path.resolve(this.options.filename);
		this.options.basedir = path.resolve(this.options.basedir || process.cwd() );
		this.options.reldir = path.dirname(path.relative(
			this.options.basedir, this.options.filename) );
		//Hide full paths for templates compiled by the Blade middleware
		if(this.options.middleware)
		{
			this.options.filename = path.relative(this.options.basedir,
				this.options.filename);
			delete this.options.basedir;
		}
		this.includes = 0;
	}
	else
		this.includes = -1;
	if(this.options.cache)
		this.options.minify = true;
	if(this.options.includeSource == null && process.env.NODE_ENV == "development")
		this.options.includeSource = true;
	this.dependencies = [];
	//Name of reserved variable in template
	this.options.templateNamespace = this.options.templateNamespace || "__";
}

/* static */ Compiler.parse = function(string) {
	return parser.parse(string);
}
Compiler.doctypes = doctypes;
Compiler.selfClosingTags = selfClosingTags;
Compiler.filters = filters;

Compiler.prototype.compile = function(cb) {
	var ns = this.options.templateNamespace;
	//Update options in runtime environment for "includes"
	runtime.compileOptions = this.options;
	if(this.options.debug)
		console.log("Compiling:\n" + this.string +
			"\n---------------------------------------------");
	try {
		this.ast = Compiler.parse(this.string);
		if(this.options.debug)
			console.log("AST:\n", require('util').inspect(this.ast, false, 15, true),
				"\n---------------------------------------------");
	} catch(e) {
		if(this.options.filename)
			e.filename = this.options.filename;
		e.message = "Parser error: " + e.message;
		e.source = this.string;
		return cb(runtime.rethrow(e) );
	}
	try {
		//Convert to JS function
		this.buf = "";
		this.blockDeclarations = false;
		this._pushOff(ns + ' = ' + ns + ' || [];' + //Define ns as an array
			ns + '.r = ' + ns + '.r || blade.Runtime;' + //Define ns.r to point to the runtime
			'if(!' + ns + '.func) ' + ns + '.func = {},' + //Define ns.func to hold all functions
				ns + '.blocks = {},' + //Define ns.blocks to hold all blocks
				ns + '.chunk = {};' + //Define ns.chunk to hold all functions chunks
			ns + '.locals = locals || {};'); //Store all locals, too
		var baseRelStart = this.buf.length;
		//Expose the filename no matter what; this is needed for branch labels if a
		//"live page update engine" is used
		this._pushOff(this.options.filename ? ns + ".filename = " +
			JSON.stringify(this.options.filename) + ";" : "");
		//Only include error handling and source code, if needed
		if(this.options.minify !== true && this.options.includeSource)
			this._pushOff(ns + '.source = ' + JSON.stringify(this.string) + ";");
		this._pushOff("\ntry {");
		//Now compile the template
		this._pushOff('with(' + ns + '.locals) {');
		for(var i = 0; i < this.ast.doctypes.length; i++)
			this._compileDoctype(this.ast.doctypes[i]);
		for(var i = 0; i < this.ast.nodes.length; i++)
			this._compileNode(this.ast.nodes[i]);
		//Close brace for `with`
		this._pushOff("}");
		//Close main `try` and insert `catch`
		if(this.options.minify !== true)
			this._pushOff("} catch(e){return cb(" + ns + ".r.rethrow(e, " + ns + ") );}");
		else
		  this._pushOff("} catch(e){return cb(e);}");
		//End of template callback
		this._pushOff('if(!' + ns + '.inc) ' + ns + '.r.done(' + ns + ');');
		if(this.blockDeclarations)
			this._pushOff(ns + '.bd = 1;');
		this._pushOff('cb(null, ' + ns + '.join(""), ' + ns + ');');
		//Add base and rel properties to buffer if includes were used
		if(this.includes > 0)
		{
			var tempBuf = this.buf.substr(baseRelStart);
			this.buf = this.buf.substr(0, baseRelStart);
			if(this.options.basedir)
				this._pushOff(ns + ".base=" + JSON.stringify(this.options.basedir) + ";");
			this._pushOff(ns + ".rel=" + JSON.stringify(this.options.reldir) + ";");
			this.buf += tempBuf;
		}
		if(this.options.debug)
			console.log("Template:\n" + this.buf +
				"\n---------------------------------------------");
		//Wrapper only accepts `locals` and `cb`
		var wrapper = function(locals, cb) {
			var info = [];
			info.r = runtime;
			wrapper.template(locals, cb, info);
		}
		try {
			//The actual template exepects `locals`, `runtime`, and `cb`
			if(this.options.debug)
				console.log("Compiling template...");
			wrapper.template = new Function("locals", "cb", ns, this.buf);
			if(this.options.debug)
				console.log("Template compiled successfully!");
			wrapper.filename = this.options.filename;
			wrapper.minify = this.options.minify;
			wrapper.dependencies = this.dependencies;
			wrapper.reldir = this.options.reldir;
			wrapper.unknownDependencies = this.unknownDependencies === true;
			wrapper.toString = function() {
				//Try to use uglify-js
				return bladeutil.uglify(this.template.toString(), this.minify);
			};
		} catch(e) {
			/* Note: The error object generated when calling `new Function(...)` does not
				contain any useful information about where the error occurred. No line
				number. No offset. Nothing.
				
				This is not only a Node.js limitation, it is a Google V8 limitation.
				I don't like this.  If you don't like this, go complain:
				
				Node.js issue: https://github.com/joyent/node/issues/2734
				Google V8 issue: http://code.google.com/p/v8/issues/detail?id=1914
			*/
			e.message = "An error occurred while generating a function from the" + 
				" compiled template. This could be a problem with Blade, or it could be a" + 
				" syntax issue with the JavaScript code in your template: " + e.message;
			throw e;
		}
	} catch(e) {
		if(this.options.filename)
			e.filename = this.options.filename;
		e.message = "Compile error: " + e.message;
		e.source = this.string;
		return cb(runtime.rethrow(e) );
	}
	cb(null, wrapper);
}

Compiler.prototype.addDoctype = function(name, value) {
	this.options.doctypes[name] = value;
};

Compiler.prototype.addFilter = function(name, filter) {
	this.options.filters[name] = filter;
};

Compiler.prototype._push = function(str) {
	if(this.inPush)
		this.buf += "+" + str;
	else
	{
		this.buf += this.options.templateNamespace + ".push(" + str;
		this.inPush = true;
	}
};
Compiler.prototype._pushOff = function(str) {
	if(this.inPush)
	{
		this.buf += ");" + str;
		this.inPush = false;
	}
	else
		this.buf += str;
}

Compiler.prototype._compileDoctype = function(doctype) {
	var ns = this.options.templateNamespace;
	if(this.options.doctypes[doctype])
		this._push(JSON.stringify(this.options.doctypes[doctype]) );
	else
		this._push(JSON.stringify('<!DOCTYPE ' + doctype + '>') );
}

Compiler.prototype._compileNode = function(node) {
	var ns = this.options.templateNamespace;
	if(this.options.minify !== true && node.line != null && (this.lastNode == null ||
		this.lastNode.type != "code" || this.lastNode.children.length == 0 ||
		node.type != "code") && node.type != "foreach_else")
	{
		this._pushOff(ns + ".line=" + node.line + "," + ns + ".col=" + node.col + ";");
		if(node.type != "code")
			this.lastNode = node;
	}
	if(this.prependNewline && node.type != "text" && node.type != "filtered_text")
		this.prependNewline = false;
	switch(node.type)
	{
		case 'tag':
			var attrs = node.attributes;
			//id attribute
			if(node.id)
			{
				/* If the tag doesn't have an "id" attribute, add it now; otherwise,
					only add it if the attribute is 'code'.
					That is, `div(id="foo")` always takes precedence over `div#foo`
				*/
				if(!attrs.id || attrs.id.text == "")
					attrs.id = {'escape': false, 'text': node.id};
				else if(attrs.id.code)
					attrs.id.code = "(" + attrs.id.code + ") || " + JSON.stringify(node.id);
			}
			//class attribute
			if(node.classes.length > 0)
			{
				/* If the tag doesn't have a "class" attribute, add it now; otherwise,
					if the "class" attribute is text, just append to it now; otherwise,
					append the classes at runtime
				*/
				if(!attrs['class'])
					attrs['class'] = {'escape': false, 'text': node.classes.join(" ")};
				else if(attrs['class'].text)
					attrs['class'].text += (attrs['class'].text.length > 0 ? " " : "") +
						node.classes.join(" ");
				else
					attrs['class'].append = node.classes.join(" "); //append classes at runtime
			}
			//event handlers
			var numEventHandlers = 0,
				eventHandlerID;
			for(var i = 0; i < node.children.length; i++)
				if(node.children[i].type == "event_handler")
				{
					//This is the first event handler, so we have some setup work to do...
					if(numEventHandlers++ == 0)
					{
						//Add id attribute, if necessary
						if(!attrs.id)
						{
							eventHandlerID = "'blade_'+" + ns + ".r.ueid";
							attrs.id = {
								"escape": false,
								"code": eventHandlerID + "++"
							};
						}
						else
							eventHandlerID = attrs.id.text ? JSON.stringify(attrs.id.text) :
								attrs.id.code;
					}
					if(node.children[i].event_handler.length == 0) continue;
					//Setup the event handler
					this._pushOff(ns + ".r.bind(" + JSON.stringify(node.children[i].events.toLowerCase() ) +
						"," + eventHandlerID + ",function(e){" +
						node.children[i].event_handler + "\n}," + ns +
						(numEventHandlers > 1 ? ",1" : "") + ");");
					//Add event attributes
					var events = node.children[i].events.split(" ");
					for(var j = 0; j < events.length; j++)
						attrs["on" + events[j].toLowerCase() ] = {
							"escape": false,
							"text": "return blade.Runtime.trigger(this,arguments);"
						};
				}
			//start tag
			this._push((node.prependSpace ? "' <" : "'<") + node.name + "'");
			//attributes
			var varAttrs = "";
			for(var i in attrs)
			{
				//interpolate text attributes
				if(attrs[i].text)
				{
					var stringified = JSON.stringify(attrs[i].text),
						interpolated = bladeutil.interpolate(stringified, ns);
					//check to see if this text attribute needs to be interpolated
					if(interpolated != stringified)
					{
						delete attrs[i].text;
						attrs[i].code = interpolated;
					}
				}
				//take care of text attributes here
				if(attrs[i].text != null)
				{
					if(attrs[i].escape)
						this._push("' " + i + "=" + bladeutil.quote(
							JSON.stringify(runtime.escape(attrs[i].text)) ) + "'");
					else
						this._push("' " + i + "=" + bladeutil.quote(
							JSON.stringify(attrs[i].text) ) + "'");
				}
				//take care of code attributes here
				else
					varAttrs += "," + JSON.stringify(i) + ":{v:" + attrs[i].code +
						(attrs[i].escape ? ",e:1" : "") +
						(i == "class" && attrs[i].append ?
							",a:" + JSON.stringify(attrs[i].append): "") + "}";
			}
			if(varAttrs.length > 0)
				this._pushOff(ns + ".r.attrs({" + varAttrs.substr(1) + "}, " + ns + ");");
			//child nodes and end tag
			if(this.options.selfClosingTags.indexOf(node.name) >= 0)
			{
				this._push("'/>'");
				if(node.children.length > numEventHandlers)
				{
					var e = new Error("Self-closing tag <" + node.name +
						"/> may not contain any children.");
					e.line = node.line, e.column = node.col;
					throw e;
				}
			}
			else
			{
				this._push("'>'");
				for(var i = 0; i < node.children.length; i++)
					if(node.children[i].type != "event_handler")
						this._compileNode(node.children[i]);
				this._push("'</" + node.name + ">'");
			}
			if(node.appendSpace)
				this._push("' '");
			break;
		case 'event_handler':
			var e = new Error("Event handlers may only be defined for a tag.");
			e.line = node.line, e.column = node.col;
			throw e;
			break;
		case 'text':
			//Ensure we prepend a newline if the last node was a text node.
			if(this.prependNewline)
				node.text = "\n" + node.text;
			//Interpolate #{stuff}
			var interpolated = bladeutil.interpolate(JSON.stringify(node.text), ns);
			//Optionally escape the text
			if(node.escape)
			{
				//If no string interpolation was used in this node, we can just escape it now
				if(interpolated == JSON.stringify(node.text) )
					this._push(JSON.stringify(runtime.escape(node.text) ) );
				//Otherwise, we'll escape it at runtime
				else
					this._push(ns + ".r.escape(" + interpolated + ")");
			}
			else
				this._push(interpolated);
			this.prependNewline = true;
			break;
		case 'code_output':
			/* This is a text block that contains code, which should be outputed into
			the view.  If the code ends in a semicolon, we just remove it for
			convenience. The extra semicolon can break things. :) */
			if(node.code_output.charAt(node.code_output.length - 1) == ";")
				node.code_output = node.code_output.substr(0, node.code_output.length - 1);
			var parens = node.code_output.indexOf(",") >= 0;
			if(node.escape)
				this._push(ns + ".r.escape(" + (parens ? "(" : "") + node.code_output +
					"\n)" + (parens ? ")" : "") );
			else
				this._push((parens ? "(" : "") + node.code_output + "\n" +
					(parens ? ")" : "") );
			break;
		case 'filtered_text':
			if(typeof this.options.filters[node.name] != "function")
			{
				var e = new Error("Invalid filter name: " + node.name);
				e.line = node.line, e.column = node.col;
				throw e;
			}
			var output = this.options.filters[node.name](node.filtered_text, {
				'minify': this.options.minify,
				'compress': this.options.minify,
				'filename': this.options.filename
			});
			//Ensure we prepend a newline if the last node was a text node.
			if(this.prependNewline)
				output = "\n" + output;
			if(this.options.filters[node.name].interpolate === false)
				this._push(JSON.stringify(output) );
			else
				this._push(bladeutil.interpolate(JSON.stringify(output), ns) );
			this.prependNewline = true;
			break;
		case 'comment':
			if(!node.hidden)
			{
				this._push("'<!--'");
				var start = this.buf.length; //Keep track of where the comment begins
				this._push(JSON.stringify(node.comment) );
				if(node.children) //unparsed comments do not have children attribute
					for(var i = 0; i < node.children.length; i++)
						this._compileNode(node.children[i]);
				//Escape all instances of "<!--" and "-->"
				var comment = this.buf.substr(start)
					.replace(/<!--/g, "<!-!-")
					.replace(/-->/g, "-!->");
				this.buf = this.buf.substr(0, start) + comment;
				//Now end the comment
				this._push("'-->'");
			}
			//if node.hidden, we ignore it entirely
			break;
		case 'conditional_comment':
			this._push("'<!--['");
			this._push(JSON.stringify(node.comment) );
			this._push("']>'");
			for(var i = 0; i < node.children.length; i++)
				this._compileNode(node.children[i]);
			this._push("'<![endif]-->'");
			break;
		case 'code':
			/* A line of code, which does not output into the view.
			If the node has children, we use the curly brace to enclose them;
			otherwise, we terminate the code with a semicolon */
			if(node.multiline)
			{
				var e = new Error("Code statements spanning mulitple lines " +
					"must be indented underneath a line of code.");
				e.line = node.line, e.column = node.col;
				throw e;
			}
			this._pushOff(node.code + "\n");
			if(node.children.length > 0)
			{
				//Process mutli-line code statements first
				for(var i = 0; i < node.children.length; i++)
				{
					if(node.children[i].type == "code" && node.children[i].multiline)
					{
						this._pushOff(node.children[i].code + "\n");
						if(node.children[i].children.length > 0)
						{
							var e = new Error("Code statements spanning mulitple lines " +
								"cannot have child nodes.\nPlease check to ensure " +
								"nothing is indented underneath the `_` line of code.");
							e.line = node.children[i].line, e.column = node.children[i].col;
							throw e;
						}
					}
					else
						break;
				}
				//Now process all other child nodes
				var insertCurlyBrace = i < node.children.length;
				if(insertCurlyBrace)
					this._pushOff("{");
				for(; i < node.children.length; i++)
					this._compileNode(node.children[i]);
				//Now we end the code statement with a curly brace or semicolon
				if(insertCurlyBrace)
					this._pushOff("}");
				else
					this._pushOff(";");
			}
			else
				this._pushOff(";");
			break;
		case 'include':
			if(this.includes == -1)
			{
				var e = new Error("Includes will not work unless the `filename` property" +
					" is passed to the compiler.");
				e.line = node.line, e.column = node.col;
				throw e;
			}
			this.includes++;
			var exposedVarList = "";
			if(node.exposing)
				for(var i = 0; i < node.exposing.length; i++)
					exposedVarList += "," + JSON.stringify(node.exposing[i]) + "," + node.exposing[i];
			if(node.code)
			{
				this._pushOff(ns + ".r.include(" + node.code + "," + ns + exposedVarList + ");");
				this.unknownDependencies = true;
			}
			else if(node.filename.length > 0)
			{
				this._pushOff(ns + ".r.include(" + JSON.stringify(node.filename) + "," +
					ns + exposedVarList + ");");
				//Add to list of dependencies
				if(this.dependencies.indexOf(node.filename) < 0)
					this.dependencies.push(node.filename);
			}
			else
			{
				var e = new Error("Invalid include statement: You must specify a filename");
				e.line = node.line, e.column = node.col;
				throw e;
			}
			break;
		case 'block':
			this.blockDeclarations = true;
			var paramStr = node.parameters == null ? "" : "," + node.parameters.join(",");
			this._pushOff(ns + ".r.blockDef(" + JSON.stringify(node.name) + ", " + ns +
				", function(" + ns + paramStr + ") {");
			for(var i = 0; i < node.children.length; i++)
				this._compileNode(node.children[i]);
			this._pushOff("});");
			break;
		case 'render':
			this._pushOff(ns + ".r.blockRender('" + node.behavior.charAt(0) + "', " +
				JSON.stringify(node.name) + ", " + ns +
				(node.arguments.length > 0 ? ", " + node.arguments : "") + ");");
			break;
		case 'append':
		case 'prepend':
			if(node.parameters != null)
			{
				var e = new Error("Parameters are not permitted for `" +
					node.type + " block`.");
				e.line = node.line, e.column = node.col;
				throw e;
			}
		case 'replace':
			var paramStr = node.parameters == null ? "" : "," + node.parameters.join(",");
			this._pushOff(ns + ".r.blockMod('" + node.type.charAt(0) + "', " +
				JSON.stringify(node.name) + ", " + ns + ", " +
				"function(" + ns + paramStr + ") {");
			for(var i = 0; i < node.children.length; i++)
				this._compileNode(node.children[i]);
			this._pushOff("});");
			break;
		case 'function':
			var paramStr = node.parameters == null ? "" : "," + node.parameters.join(",");
			this._pushOff(ns + ".r.func(" + JSON.stringify(node.name) + ",function(" +
				ns + paramStr + ") {");
			/* If the first child node is a tag, ensure that its attributes are modified
				to allow a user to pass in an id or classes when the function is called */
			if(node.children.length > 0 && node.children[0].type == "tag")
			{
				var attrs = node.children[0].attributes;
				modifyAttribute('id');
				modifyAttribute('class');
				function modifyAttribute(attrName) {
					//If the attribute does not exist, simply add it as code; no need to escape
					if(attrs[attrName] == null)
						attrs[attrName] = {'escape': false, 'code': "this." + (attrName == "class" ? "classes" : attrName) };
					//Merge passed-in classes with the class attribute
					else if(attrName == "class")
					{
						if(attrs[attrName].code)
							attrs[attrName].code = "(this.classes || []).concat(" + attrs[attrName].code + ")";
						else
						{
							attrs[attrName].code = "(this.classes || []).concat(" +
								bladeutil.interpolate(JSON.stringify(attrs[attrName].text) ) + ")";
							delete attrs[attrName].text;
						}
					}
					//Replace the attribute with the passed-in attribute value
					else if(attrs[attrName].code)
						attrs[attrName].code = "this." + attrName + '||' + attrs[attrName].code;
					else
					{
						attrs[attrName].code = "this." + attrName + '||' +
							bladeutil.interpolate(JSON.stringify(attrs[attrName].text) );
						delete attrs[attrName].text;
					}
				}
			}
			for(var i = 0; i < node.children.length; i++)
				this._compileNode(node.children[i]);
			//The buffer length before the function call is returned by the function
			this._pushOff("}," + ns + ");");
			break;
		case 'call':
			//The buffer length before the function call is returned by the function
			if(node.output)
				this._pushOff(node.output.to + (node.output.append ? "+=" : "=") + ns +
					".r.capture(" + ns + "," + ns + ".length,");
			if(node.arguments != "")
				node.arguments = "," + node.arguments;
			this._pushOff(ns + ".r.call(" + JSON.stringify(node.name) + ",{" +
				(node.id ? "id:" + JSON.stringify(node.id) + "," : "") +
				(node.classes.length > 0 ? "classes:" +
					JSON.stringify(node.classes) + "," : "") +
				"}," + ns + node.arguments + (node.output ? ") );" : ");") );
			break;
		case 'chunk':
			console.warn("Blade chunks are now deprecated. Please fix " +
				(this.options.filename ? this.options.filename + ":" : "line ") +
				node.line + ":" + node.col);
			var paramStr = node.parameters == null ? "" : node.parameters.join(",");
			this._pushOff(ns + ".r.chunk(" + JSON.stringify(node.name) + ",function(" +
				paramStr + ") {");
			for(var i = 0; i < node.children.length; i++)
				this._compileNode(node.children[i]);
			this._pushOff("}," + ns + ");");
			break;
		case 'isolate':
			this._pushOff(ns + ".r.isolate(function() {");
			for(var i = 0; i < node.children.length; i++)
				this._compileNode(node.children[i]);
			this._pushOff("}," + ns + ");");
			break;
		case 'constant':
			this._pushOff(ns + ".r.constant(" + node.line + ",function() {");
			for(var i = 0; i < node.children.length; i++)
				this._compileNode(node.children[i]);
			this._pushOff("}," + ns + ");");
			break;
		case 'preserve':
			if(!node.preserved)
				node.preserved = "[]";
			this._pushOff(ns + ".r.preserve(" + node.line + ",(" + node.preserved + ")||[],function() {");
			for(var i = 0; i < node.children.length; i++)
				this._compileNode(node.children[i]);
			this._pushOff("}," + ns + ");");
			break;
		case 'foreach':
			this._pushOff(ns + ".r.foreach(" + ns + "," + node.cursor + ",function(" +
				(node.itemAlias ? node.itemAlias : "") + ") {");
			for(var i = 0; i < node.children.length; i++)
				this._compileNode(node.children[i]);
			this._pushOff("});");
			break;
		case 'foreach_else':
			if(!this.lastNode || this.lastNode.type != "foreach")
			{
				var e = new Error("No matching foreach list block. You cannot put a foreach else block here!");
				e.line = node.line, e.column = node.col;
				throw e;
			}
			//Remove trailing ");" and add elseFunc argument to the Runtime.foreach(...) call.
			this.buf = this.buf.substr(0, this.buf.length - 2) + ",function() {";
			for(var i = 0; i < node.children.length; i++)
				this._compileNode(node.children[i]);
			this._pushOff("});");
			break;
		case 'blank_line':
			//Ignore these lines
			break;
		default:
			var e = new Error("Unknown node type: " + node.type);
			e.line = node.line, e.column = node.col;
			throw e;
			break;
	}
	if(this.prependNewline && node.type != "text" && node.type != "filtered_text")
		this.prependNewline = false;
	this.lastNode = node;
}
