/*
	TODO:
	
	1) Grouping in (..)

	2) snippets.json - Plus char problem

			"colg+": "colgroup>col",
			"colgroup+": "colgroup>col",
			"dl+": "dl>dt+dd",
			"map+": "map>area",
			"ol+": "ol>li",
			"optg+": "optgroup>option",
			"optgroup+": "optgroup>option",
			"pic+": "picture>source:srcset+img",
			"select+": "select>option",
			"table+": "table>tr>td",
			"tr+": "tr>td",
			"ul+": "ul>li"
*/




!function(that, fun){

	if (typeof define === 'function' && define.amd) {
		define(fun);
	} else if (typeof exports === 'object') {
		module.exports = fun();
	} else {
		that.ZenDecode = fun();
	}

}(this, function(){

	/**
	 * Class constructor
	 * 
	 * @param {string or array} input String or array with
	 *                   							zen abbreviation.
	 * @param {array} 					args 	Arguments:
	 *                          {
	 *                           renderType		html, css, xml rtc..
	 * 					                 tree 				Tags tree prepared 
	 * 					                 							to render.
	 * 					                 snippetsObj 	Object with snippets
	 *                           snippetsFile JSON File with snippets.
	 *                          }
	 * 
	 */
	var ZenDecode = function (input, args) {

		args = args || {};
		this.renderType = args.renderType || 'html';
		this.tree = args.tree;
		input = input || '';

		/*
		 * Prevent this polluting of the namespace
		 * (Run this only like a constructor).
		 */
		if (!(this instanceof ZenDecode))
	  	return new ZenDecode(input, args);

	  /*
	   * Load snippets files
	   */
	  if (typeof args.snippetsObj==='undefined') {
	  	if (typeof require!=='undefined') {
	  		this.snippets	= require('./tags.json');
	  		// this.snippets	= require(args.snippetsFile || './tags.json');
	  	} else {
	  		// Error: No snippets object. //
					throw {
					name: 'ZenDecode error',
					message: 'No snippets object'
				};
	  	}
	  } else {
	  	this.snippets = args.snippetsObj;
	  }

	  /*
	   * Prepare input variable.
		 * Convert string type input to array.
	   */
	  setInput(this, input);

		/*
		 * Create tree if input exist.
		 */
		if (!this.tree) {
			this.tree = createTree(this);
		}

	};


	/**
	 * Class methods
	 * @type {Object}
	 */
	ZenDecode.prototype = {

		createTree: function (input){
			if (input) {
			  /*
			   * Prepare input variable.
				 * Convert string type input to array.
			   */
			  setInput(this, input);
			}

			this.tree = createTree(this);
			return this;
		},

		render: function () {
			return render(this.tree);
		},

	};


	/**
	 * Set input abbreviation.
	 * 
	 * @param {object} 					that 	Class 'this' object.
	 * @param {string or array} input String or array with
	 *                   							zen abbreviation.
	 */
	function setInput (that, input) {

		/*
		 * Prepare input variable.
		 * Convert string type input to array.
		 */
		if (Array.isArray(input)) {
			// Continue
			that.input = input;
		}else if (typeof input === 'string') {

			// Replece
			input = replaceBeforeFlat(input, that.snippets, that.renderType);
			input = input.replace(/>/g, '@@_1@@_');
			input = input.replace(/\+/g, '@@_');
			input = input.replace(/\^/g, '@@_-1@@_');
			input = input.split('@@_');
			input = input.filter(function (n){ return n !== ''; });
			that.input 	= input;


			// // TEST
			// var re_group 	= /(>)(?!.*>)(.*?)(?:\^|$)/;
			// var re_inside	= /\{(.*?)\}/g;
			// var re_plus 	= /\+/g;

			// // str = str.replace(re_inside, '"string_inside": "$1"');
			// str = str.replace(re_inside, '"$1"');

			// while(re_group.test(str)) {
			// 	str = str.replace(re_group, ': {$2}, ');
			// }

			// str = str.replace(re_plus, ', ');
			// str = '{ '+str+' }';
			// str = str.replace(/,\s*(?=})/, '');

		}else{
			// Error: Wrong input. //
			throw {
				name: 'ZenDecode error',
				message: 'Wrong input'
			};
		}

	}

	/**
	 * Replace snippets before building tree.
	 * 
	 * @param  {string} input    Unprocessed string.
	 * @param  {object} snippets Snippets object from file.
	 * @param  {string} type     Snippets type ex.'html', 'css' etc.
	 * @return {string}          Ready string to create a tree.
	 */
	function replaceBeforeFlat (input, snippets, type){

		for (var snipp in snippets[type].replace) {

			// snipp = snipp.replace(/(\!)/g, '\\$1');

			var re = new RegExp('((?:>|\\^|\\+|^))'+snipp+'\\b');
			if (re.test(input)) {
				input = input.replace(re, '$1'+snippets[type].replace[snipp]);
				replaceBeforeFlat(input, snippets, type);
			}
		}

		// type = type || 'html';
		// array.forEach(function (element, index, array){

		// 	var el = snippets[type]['replace'][element];
		// 	while(el){
		// 		if(el){
		// 			array[index] = el;
		// 		}
		// 		el = snippets[type]['replace'][el];
		// 	}

		// });

		return input;
	}


	/**
	 * Construct single tree node from zen command string.
	 * 
	 * @param  {object} that Class 'this' object.
	 * @param  {string} str  Zen command string.
	 * @param  {number} deep Node level in tree.
	 * @return {object}      Prepared node object.
	 *                       { 
	 *                        tag 		Tag name
	 *												id 			Tag ID
	 *												class 	Tag class array
	 *												args 		Tag argiments object
	 *												content Tag content array
	 *												type    Type of object ex: tag/base
	 *												deep 		Node deep level.
	 *												multiply The number of repetitions.
	 *											 }
	 */
	function treeNode (that, str, parent, deep) {

		/*
		 * Get zen arguments.
		 */
		var tagName 		= str.match(/^(.*?)(?=#|\.|>|\[|\*|{| |$)/i);
		var tagId		 		= str.match(/#(.*?)(?=#|\.|>|\[|\*|{| |$)/i);
		var tagClass 		= str.match(/\.(.*?)(?=#|\.|>|\[|\*|{| |$)/gi);
		var tagArgs 		= str.match(/\[(.*)\]/);
		var tagString 	= str.match(/{(.*?)}/);
		var tagMultiply = str.match(/\*(\d+)/);

		/*
		 * Convert zen arguments to tag arguments.
		 */
		var block = null;
		if(tagName!==null){
			block = tagName[0].match(/(.*)(?=\:)/);
			tagName = tagName[0];
			if (block) {
				block = block[0];
			}else{
				block = tagName!=='' ? tagName : parent.block==='p' ? 'p' : 'div';
			}
		}else{
			block = parent.block==='p' ? 'p' : 'div';
		}
		if(tagId!==null){
			tagId = tagId[1];
		}
		if(tagClass!==null){
			tagClass.forEach(function (element, index, array){
				array[index] = element.replace(/^\./, '');
			});
		}
		
		/*
		 * Default tag arguments.
		 */
		var args = {};
		var single = false;
		if (that.snippets[that.renderType].defaults[tagName]!==undefined) {
			args = that.snippets[that.renderType].defaults[tagName].args;
			if (that.snippets[that.renderType].defaults[tagName].single!==undefined) {
				single = that.snippets[that.renderType].defaults[tagName].single;
			}
		}

		/*
		 * Merging manual and default arguments.
		 */
		if(tagArgs!==null){
			tagArgs = tagArgs[1];
			tagArgs = tagArgs.replace(/\"(.|(\s))*?\"/g, function replacer(match, x) {
			  return match.replace(' ', '@_space_@');
			});
			tagArgs = tagArgs.split(' ');
			tagArgs.forEach(function (element, index, array){
				element = element.replace('@_space_@', ' ');
				var arg = element.split('=');
				if(arg.length){
					if(arg[1]!=='""' && arg[1]===''){
						args[arg[0]] = '';
					}else{
						args[arg[0]] = arg[1];
					}
				}else{
					args[arg[0]] = null;
				}
			});
			// tagArgs = args;
		}
		if(tagString!==null){
			tagString = tagString[1];
		}
		if(tagMultiply!==null){
			tagMultiply = tagMultiply[1];
		}

		/* 
		 * Create node with arguments
		 */
		var node;
		var inlineTags = ['b','big','i','small','tt','abbr','acronym','cite','code','dfn','em','kbd','strong','samp','time','var','a','bdo','br','img','map','object','q','script','span','sub','sup','button','input','label','select','textarea'];

		if(tagName==='' && tagClass===null && tagId===null){
			if(tagString){
				node = {
					type: 'string',
					content: tagString,
				};
			}
		}else{
			node = {
				block: block,
				id: tagId || undefined,
				class: tagClass || undefined,
				args: args || undefined,
				content: [],
				single: single,
				parent: parent,
				type: 'tag',
				isInline: inlineTags.indexOf(block)>-1 ? true : false,
				deep: deep,
				multiply: tagMultiply || 1,
			};
			if(tagString){
				node.content.push({
					type: 'string',
					content: tagString,
				});
			}
		}

		return node;
	}


	/**
	 * Function processes flat array to multidimensional 
	 * object array. Create tree of tags with arguments.
	 * 
	 * @param  {object} that  	 Class 'this' object.
	 * @param  {array} 	input  	 Strings array contain 
	 *                           commands for tree levels 
	 *                           and tag contents (arguments).
	 * @param  {object} parent   Object with 'content' property.
	 * @param  {object} lastNode Reference to last node.
	 * @param  {number} deep     Number of current level 
	 *                           (0 is base level).
	 * @return {object}          Return parent object with 
	 */
	function createTree (that, args) {

		var input 		= that.input;
		args 					= args 						|| {};
		var parent 		= args.parent 		|| {content: []};
		var lastNode 	= args.lastNode 	|| parent;
		var deep 			= args.deep 			|| 0;
		if (!parent.content) {
			parent[content] = [];
		}
		if (typeof input==='undefined' ) {
			return;
		}

		/*
		 * Go through flat array.
		 */
		while(input.length>0){

			/*
			 * 1. Get command or node from array
			 */
			var str = input.shift();

			/*
			 * 2. Construct levels of a tree.
			 *      	- the same lavel (nothing)
			 *     1  - 1new level
			 *    -1  - level before
			 * 
			 */
			if(!isNaN(parseFloat(str))){
				if(str=='-1' && deep>0){
					return;
				}
				if(str==='1'){
					args.parent 		= lastNode;
					args.lastNode 	= lastNode;
					args.deep 			= deep+1;
					createTree(that, args);
				}

			/*
			 * 3. Create node
			 */
			}else{
				var node = treeNode(that, str, parent, deep);
				if(node){
					parent.content.push(node);
					if(node.type!=='string'){
						lastNode = node;
					}
				}
			}
		}

		return parent;
	}


	/**
	 * Convert tree to string.
	 * 
	 * @param  {object} node    Tree node.
	 * @param  {nymber} deep    Node deep level.
	 * @param  {number} index   Index number if multiple used.
	 * @param  {number} maximum Multiplier value.
	 * @return {string}         Converted string.
	 */
	function render (node, deep, index, maximum) {

		function addAttribute (element, index, array){
			if(index!==0){
				out += ' ';
			}
			out += element.dolar(idx, max);
		}
		function addContent (element, index, array) {
			var inside = '';
			if(element.type==='tag'){
				inside = render(
										element,
										deep+1,
										idx,
										max
					);
			}else if(element.type==='string'){
			 	inside = element.content.dolar(idx, max);
			}
			out += inside;
		}

		var format = true;
		var single = false;

		if (!node)		return;
		deep = deep || 0;
		var indent = '\t'.repet(deep);
		var multiply = node.multiply ? node.multiply : 1;
		var max = multiply>1 ? Number(multiply) : maximum;
		var out = '';

		for (var i=0; i<multiply; i++) {
			var idx = multiply>1 ? i : index;
			if (node.type==='tag') {

				/*
				 * Check if this is single tag, without closing.
				 */
				single = false;
				if (node.content.length===0 && node.single && node.single===true) {
					single = true;
				}

				/*
				 * 1. Create tag
				 */
				// Format outour sting. //
				if (format && !node.parent.isInline && node.parent.block!=='p') {
					out += indent;
				}

				out += '<';
				out += node.block;
				out += node.id ? ' id="'+node.id.dolar(idx, max)+'"' : '';
				if(node.class){
					out += ' class="';
					node.class.forEach(addAttribute);
					out += '"';
				}
				if(node.args){
					for(var property in node.args){
						out += ' ';
						if(node.args[property]===null){
							out += property;
						}else	if(node.args[property]===''){
							out += property+'=""';
						}else{
							out += property+'='+node.args[property].dolar(idx, max);
						}
					}
				}

				// Single tag, without closing. //
				if(single){
					out += '/';
				}
				out += '>';

				// Format outour sting. //
				if (format && (single || (!node.isInline && node.block!=='p'))) {
					out += '\n';
				}
			
			}

			/*
			 *  2. Content.
			 */
			if(node.content){
				 node.content.forEach(addContent);
			}

			/*
			 * 3. Close tag.
			 */
			if(node.type==='tag' && !single){
				if (format && !node.isInline && node.block!=='p') {
					out += indent;
				}
				out += '</';
				out += node.block || '';
				out += '>';

				// Format outour sting. //
				if (format && !node.parent.isInline && node.parent.block!=='p') {
					out += '\n';
				}
			}
		}

		return out;
	}


	/**
	 * Addition function to string class.
	 * Repetition this string multiple times.
	 * 
	 * @param  {number} multiplier 	Multiplier.
	 * @return {string}      				Repeated string.
	 */
	String.prototype.repet = function(multiplier){
		return (new Array(multiplier).join(this));
	};


	/**
	 * Addition function to string class.
	 * Replace dolars to loop index value.
	 * 
	 * @param  {number} idx Loop index value.
	 * @param  {number} max Maximum index value.
	 * @return {string}     Return finished string.
	 */
	String.prototype.dolar = function(idx, max){
		if(this!=='' && idx!==undefined){
			return this.replace(/(\$+)((@-?\d*)?)/g, function replacer(match, value, reverse) {
				reverse = reverse || '';
				var offset = reverse.match(/\d+/);
				idx = idx+1;
				max = max+1;
			  var val = /-/.test(reverse) ? max-idx : idx;
				val += Number(offset);
			  var s = "000000000" + val;
				return s.substr(s.length-value.length);
			});
		}

		return this;
	};

	return ZenDecode;
});

