/** var CompiledTpl = pulp.template; */
pulp.Modules.template = 'CompiledTpl';

(function() {

  var $p = pulp.base;

  /**
   * Create a template with variable display and basic logic and looping
   * @name pulp.template
   * @requires base
   * @requires cls
   * @example
   *   <?php ob_start();?> 
   *     {if $data.colors.length}
   *       <ul>
   *         {foreach $data.colors as $i => $color}
   *           <li class="{echo ($i % 2) == 0 ? 'even' : 'odd'}">{$color.toUpperCase()}</li>
   *         {/foreach}
   *       </ul>
   *     {else}
   *       <p>No Colors Found</p>
   *     {/if}
   *   <?php $html = ob_get_clean();?>
   *   <script type="text/javascript">
   *     var html = '<?php str_replace("'", "\\'", $html)?>';
   *     var tpl = new pulp.template(html);
   *     var colorData = {colors: ['red', 'green', 'blue']};
   *     $(myResults).innerHTML = tpl.evaluate(colorData);
   *   </script>
   */
  var tpl = pulp.template = pulp.cls.create(/** @lends pulp.template# */{
    /**
     * Construct new instance--Optionally set and compile the template for later evaluation
     *
     * @constructs
     * @param {String} [template=""]  The template to interpolate
     * @param {Object} [options]  Parser syntax options
     * @param {String} [options.open="{"]  The open tag (e.g. "#{", "<?", "<%", etc.)
     * @param {String} [options.close="}"]  The close tag (e.g. "#}, "?>", "%>", etc.) 
     * @param {String} [options.variable="$"]  The variable character(s) (e.g. "$", "&", "@", etc.)
     * @param {String} [options.assign={}]  The initial assignments if any
     */    
    initialize: function(template, options) {
    	options = options || { };
    	/**
    	 * Internal hash of assignments
    	 * @name pulp.template#_assignments
    	 * @type {Object}
    	 */
    	this._assignments = options.assign || { };
    	
      /**
       * The variable character(s) (e.g. "$", ":", "@", etc.)
       * @name pulp.template#_variable
       * @type {String}
       */
    	this._variable = options.variable || tpl.defaultOptions.variable;
      /**
       * The open tag (e.g. "#{", "<?", "<%", etc.)
       * @name pulp.template#_openTag
       * @type {String}
       */    	
    	this._openTag = tpl._regExpEscape(options.open || tpl.defaultOptions.open);
      /**
       * The close tag (e.g. "}, "?>", "%>", etc.)
       * @name pulp.template#_closeTag 
       * @type {String}
       */       	
    	this._closeTag = tpl._regExpEscape(options.close || tpl.defaultOptions.close);
    	var escapedVariable = tpl._regExpEscape(this._variable);			
    	var regex = '(' + this._openTag +
    		// allow {/*} or {*/} or {tagname expression} or {/tagname} or {$var} or {$var.modifier()}
    		// and tagnames must be 2 to 20 characters beginning with a letter
    		// and variables must begin with a letter
    		 '(?:\\/\\*|\\*\\/|[a-z][\\w_-]{1,20}.*?|\\/[a-z][\\w_-]{1,20}|' + escapedVariable + '[a-z][^' + this._closeTag + ']*)' + 
    		 this._closeTag + ')';
      /**
       * The regular expression used to separate tags and content in between
       * @name pulp.template#_tagSplitter
       * @type {RegExp}
       */     	
    	this._tagSplitter = new RegExp(regex, 'i');
      /**
       * The regular expression used to match variables referenced within tag attributes
       * @name pulp.template#_tagSplitter
       * @type {RegExp}
       */       
    	this._varMatcher = new RegExp(escapedVariable + '([a-z][\\w]*)', 'g');			
      /**
       * A String consisting of JavaScript code, that, when executed, returns the evaluated template
       * @name pulp.template#_compiled
       * @type {String}
       */       
      this._compiled = '';
    	
    	// compile the template if given
    	if (template) {
    		this.compile(String(template));
    	}
    },
    /**
     * Set and compile the template for later evaluation
     *
     * @param {String} [template=""]  The html or simple-string template
     * @return {pulp.template}
     * @throws {Error}  When template contains an unrecognized tag
     */
    compile: function(template) {
      this._compiled = '';
      
      // to facilitate tracking line no, normalize line endings to \n
      template = template.replace((/(\r\n|\r)/g), '\n');
      // Split the template into a stack of text and tokens
      var stack = tpl._splitAndCapture(template === undefined ? '' : template, this._tagSplitter);
      /**
       * The line number of the current tag
       * @name pulp.template#_lineNo
       * @type {Number}
       */
      this._lineNo = 1;
      for (var i = 0, length = stack.length; i < length; i++) {
        // odd items are tokens
        if (i % 2 == 1) { // odd
          // we jave a token: test which kind
          if (stack[i].substring(this._openTag.length, this._variable.length) == this._variable) {
            // we have a simple variable: just concatenate
            this._compiled += tpl._tagHandlers._variable.call(this, stack[i].substring(this._openTag.length - 1, stack[i].length - this._closeTag.length + 1));
          } else {
            match = /\{(\S+)(?:\s(.+))?\}/.exec(stack[i]);
            // we have a language construct: find our handler
            if (typeof tpl._tagHandlers[match[1]] == 'function') {
              // execute handler  
              this._compiled += tpl._tagHandlers[match[1]].call(this, match[2]);
            } else {
              // error: handler not found
              throw new Error('pulp.template Exception: tag name "' + (match[1] || stack[i]) + '" not recognized on line ' + this._lineNo);
            }
          }
        // for speed, ensure the content isn't an empty string
        } else if (stack[i] != '') {
          // Concatenate plain template text
          this._compiled += tpl._tagHandlers._content.call(this, stack[i]);
        }
        this._updateLineNo(stack[i]);
      }
      this._lineNo = 1;
      return this;
    },
    /**
     * Assign a value to use every time evaluate() is called for this instance
     *
     * @param {String} name  The property to which to assign the value
     * @param {Mixed} value  The value to assign
     * @return {pulp.template}
     * @chainable
     */
    assign: function(name, value) {
      this._assignments[name] = value;
      return this;
    },
    /**
     * Assign multiple values to use every time evaluate() is called for this instance
     *
     * @param {String} name  The property to which to assign the value
     * @param {Mixed} value  The value to assign
     * @return {pulp.template}
     * @chainable
     */    
    assignAll: function(assignments) {
      $p.extend(this._assignments, assignments);
      return this;
    },
    /**
     * Remove an assignment
     *
     * @param string name  The assignment to remove
     * @return {pulp.template}
     * @chainable
     */
    unassign: function(name) {
      delete this._assignments[name];
      return this;
    },
    /**
     * Remove all asignments
     *
     * @return {pulp.template}
     * @chainable
     */
    unassignAll: function() {
      this._assignments = {};
      return this;
    },
    /**
     * Evaluate the template given an object
     *
     * @param object obj  The objects whose properties to substitute for tokens
     * @param bool execScripts  If true, run {script} tag functions 10ms after template evaluation (default=true)
     * @return string
     */
    evaluate: function(obj, execScripts) {
      // add in our custom assignments
      obj = $p.extend({}, this._assignments, obj || {});
      // initialize our template output
      output = '';
      // evaluate the compiled template in the presence of our object assignments
      eval(this._compiled);
      // return the output
      return output;
    },
    /**
     * Return the raw JavaScript compilation used for eval
     */
    getCompilation: function() {
      return this._compiled;
    },
    /**
     * Replace variables and allow modifiers to be used (e.g. $name.toUpperCase())
     *
     * @param string expression  The template code
     * @return string
     */
    _interpretVars: function(expression) {
      return expression.replace(this._varMatcher, "$p.castAsString(obj.$1)");
    },
    /**
     * Convert field=value pairs into an object
     *
     * @param string expression
     * @return object
     *
     * @example
     * this._extractParams('a=1 b=2 c=3'); // {a: 1, b:2, c: 3}
     * this._extractParams('a="1" b="2" c="3"'); // {a: 1, b:2, c: 3}
     * this._extractParams('a = "1" b = "2" c = "3"'); // {a: 1, b:2, c: 3}   
     */
    _extractParams: function(expression) {
      var stack = tpl._splitAndCapture(expression, /([a-zA-Z][\w_]* ?=)/);
      var params = {}, param, value;
      for (var i = 0, length = stack.length; i < length; i++) {
        if (i % 2 == 1) { // odd
          param = $p.trim(stack[i].substring(0, stack[i].length - 1));
        } else {
          value = $p.trim(stack[i]);
          if (value) {
            params[param] = this._replaceVar(value.replace((/^("|')(.*)\1$/g), '$2'));
          }
        }
      }
      return params;
    },    
    /**
     * Replace variables and DO NOT allow modifiers to be used (e.g. $name.toUpperCase())
     *
     * @param string string
     * @return string
     */   
    _replaceVar: function(expression) {
      return expression.replace(this._varMatcher, "obj.$1");
    },
    /**
     * Update the line number by counting the number of newlines
     * @param {String} str
     * @return {undefined}
     */
    _updateLineNo: function(str) {
      this._lineNo += (str.length - str.replace((/\n/g), '').length);  
    }
  });	
  
  tpl.extend(/** @scope pulp.template */{
  	/**
  	 * Default options for template syntax
  	 * @type {Object}
  	 */
  	defaultOptions: {
  	  open: '{',
  	  close: '}',
  	  variable: '$'
  	},
    /**
     * Add or reset a tag handling function
     * 
     * @note  tags must be 2 to 16 characters beginning with a letter
     * @param string name  The tag name
     * @param function callback  The handling function
     * @return pulp.template object
     */
    setTagHandler: function(name, callback) {
      tpl._tagHandlers[name] = callback;
      return this;
    },  	
    // helper to create input tags for radio boxes
    _helpRadioCheckbox: function(type, expression) {
      var params = this._extractParams(expression);
      return '(function(name, values, currOutput, options, selected, separator) {\n' +
        tpl._helpRadioCheckboxOptions('checked') + 
        'output += \'<label><input type="' + type + '" name="\' + name + \'" value="\' + $p.escapeHTML(value) + \'"\' + sel + " /> " + $p.escapeHTML(options[value]) + "</label>" + separator + "\\n";\n' +
        '} })(' + tpl._addQuotesIfNeeded(params.name || 'radio') + ',' + this._interpretVars(params.values || 'null') + ',' + this._interpretVars(params.currOutput || 'null') + ',' + this._interpretVars(params.options || 'null') + ',' + this._interpretVars(params.selected || '[]') + ',' + tpl._addQuotesIfNeeded(params.separator || '<br />') + ');\n'
      ;
    },
  
    // helper to create iteration code for radios, checkboxes, and option tags
    _helpRadioCheckboxOptions: function(type) {
      return 'if (values && currOutput) {\n' +
        'var options = {};\n' +
        'for (var i = 0, length = values.length; i < length; i++)\n' +
        'options[values[i]] = currOutput[i];\n' +
        '} else {\n' +
        'var options = options || {};\n' +
        '}\n' +       
        'var sel;\n' +      
        'for (var value in options) {\n' +
        'if ($p.isArray(selected)) sel = ($p.inArray(selected, value) ? \' ' + type + '="' + type + '"\' : "");\n' +
        'else sel = (String(selected) === String(value) ? \' ' + type + '="' + type + '"\' : "");\n'
      ;
    },
    /**
     * Wrap a string with quotes if not present
     *
     * @param string string
     * @return string
     */   
    _addQuotesIfNeeded: function(string) {
      return (/^(['"]).*\1/.test(string) ? string : '"' + string.replace('"', '\\"') + '"');      
    },
    /**
     * Escape a string for use with single quote and single lines
     *
     * @param string string
     * @return string
     */       
    _escapeQuotes: function(string) {
      return string.replace(/'/g, "\\'").replace(/\n/g, "\\n");
    },
    /**
     * 
     */
    _regExpEscape: function(str) {
      return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
    },
    _splitAndCapture: function(subject, splitter) {
      return subject.split(splitter);
    },
  	/**
  	 * A function to handle each tag name, variables, and content.
  	 *   Each handler must return a string containing javascript code.
  	 *   Content is contained in the variable "output" and thus can be manipulated.
  	 * @type {Object}
  	 */
  	_tagHandlers: /** @scope pulp.template._tagHandlers */{
  		/**
  		 * Handler for adding simple variables to the template output
  		 */
  		'_variable': function(expression) {
  			return "output += " + this._replaceVar(expression) + ";\n";
  		},
  		/**
  		 * Handler for adding plain content to the template output
  		 */
  		'_content': function(content) {
  		  if (this.isInEval) {
  		    return content;
  		  } else {
    			// escape content for strings
    			return "output += '" + tpl._escapeQuotes(content) + "';\n";
  		  }
  		},
  		/**
  		 * Handler for comment tag open
  		 */
  		'/\*': function() { return '\n/*\n' },
      /**
       * Handler for comment tag close
       */  		
  		'*\/': function() { return '\n*/\n' },
  		/**
  		 * If Logic: an if clause followed by zero or more elseif clauses optionally followed by else
  		 *
  		 * @syntax  {if $bool} content {/if}
  		 *   adds content if $bool is true
  		 *
  		 * @syntax  {if $bool1} content1 {else} content2 {/if}
  		 *   adds content1 if $bool1 is true, content2 otherwise
  		 *
  		 * @syntax  {if $bool1} content1 {elseif $bool2} content2 {else} content3 {/if}
  		 *   adds content 1 if $bool1 is true, content2 if $bool2 is true, content3 otherwise
  		 *   
  		 * @throws {Error} When "if" or "elseif" tag contains no expression
  		 */
  		'if': function(expression) {
  			if (!expression) throw new Error('pulp.template Exception: {if} tag cannot be blank on line ' + this._lineNo);
  			return 'if (' + this._interpretVars(expression) + ') {\n';
  		},
  		'elseif': function(expression) {
  			if (!expression) throw new Error('pulp.template Exception: {if} tag cannot be blank on line ' + this._lineNo);
  			return '} else if (' + this._interpretVars(expression) + ') {\n';
  		},
  		'else': function() {
  			return '} else {\n';
  		},
  		'/if': function() {
  			return '}\n';
  		},
  		/**
  		 * "foreach" Looping: for loop or for-in loop
  		 *
  		 * @example {foreach $items}
  		 *   execute one time for each item in $items
  		 *
  		 * @example {foreach $items as $item}
  		 *   execute one time for each item in $items, with a reference to the current item in $item
  		 *
  		 * @example {foreach $items as $idx => $item}
  		 *   execute one time for each item in $items, with a reference to the iteration number in $idx and the current item in $item
  		 *
  		 * @example {foreach $property in $object}
  		 *   execute one time for each property in $object, with a reference to the property name in $property
  		 *
  		 * @example {foreach $property in $object as $item}
  		 *   execute one time for each property in $object, with a reference to the property name in $property and the property value referenced in $item
  		 */
  		'foreach': function(options) {
  			var compiled = '', alias, varName;
  			// find output if a "for" or "for in" loop is desired
  			var type = options.split(' in ');
  			if (type[1]) {
  				// "for in" loop
  				// get alias if present
  				alias = type[1].split(' as ');
  				varName = this._replaceVar(alias[0]);
  				compiled += '(function() { for (var property in ' + varName + ') {\n';
  				compiled += this._replaceVar(type[0]) + ' = property;\n';
  				// do we have an alias?
  				if (alias[1]) {
  					// yes, assign the item name to the value of this property
  					compiled += this._replaceVar(alias[1]) + ' = ' + this._interpretVars(type[0]) + '[property];\n';
  				}
  			} else {
  				// "for" loop
  				// get alias if present
  				alias = type[0].split(' as ');
  				varName = this._replaceVar(alias[0]);
  				compiled += '(function() { for (var i = 0, len = (' + varName + ' ? ' + varName + '.length : 0); i < len; i++) {\n';
  				// do we have an alias?
  				if (alias[1]) {
  					// yes alias
  					// get pair or single
  					var pair = alias[1].split(' => ');
  					// do we have a pair?
  					if (pair[1]) {
  						// yes, assign the first item name to i
  						compiled += this._replaceVar(pair.shift()) + ' = i;\n';  
  					}
  					// add the alias to the item
  					compiled += this._replaceVar(pair[0]) + ' = ' + varName + '[i];\n';
  				}
  			}
  			return compiled;
  		},
  		'/foreach': function() {
  			return '}})();\n';
  		},
  		/**
  		 * Include the output of another template object
  		 *
  		 * @example - allows encapsulation or recursion
  		 *   var tpl1 = new pulp.template('{$msg}');
  		 *   tpl1.assign('msg', 'Hello World');
  		 *   var tpl2 = new pulp.template('My message: {include $template}');
  		 *   tpl2.assign('template', tpl1);
  		 *   tpl2.evaluate(); // "My message: Hello World"
  		 */
  		'include': function(expression) {
  			return 'output += ' + this._replaceVar(expression) + '.evaluate(this._assignments);\n';
  		},
  		/**
  		 * Capture output to be used later
  		 * 
  		 * @example - create a row of empty columns
  		 *   {capture name="emptyTd"}
  		 *     <td>N/A</td>
  		 *   {/capture}
  		 *   {foreach $columns as $column}
  		 *     {if $column}
  		 *       <td>{$column}</td>
  		 *     {else}
  		 *       {$emptyTd}
  		 *     {/if}
  		 *   {/foreach}
  		 *   
  		 *   Alternate usage: {capture emptyTd} OR {capture}
  		 */
  		'capture': function(expression) {
  		  this._captureName = this._extractParams(expression).name || expression || 'global';
  			return 'obj.' + this._captureName + ' = output;\n' +
  				"output = '';\n"
  			;
  		},
  		'/capture': function(expression) {
  			return 'var preCaptureOut = obj.' + this._captureName + ';\n' +
  				'obj.' + this._captureName + ' = output;\n' + 
  				'output = preCaptureOut;\n'
  			;
  		},
  		/**
  		 * Assign a variable to be used later
  		 * 
  		 * @example - process an upper-case value once
  		 *   {assign $upperName = $name.toUpperCase()}
  		 *   {$upperName}
  		 *   
  		 * @example - process using a function (note the optional $ on the assignee
  		 *   {assign processed=myGlobalFn($value)}
  		 *   {$processed}
  		 */		
  		'assign': function(expression) {
  			var parts = expression.split(/\s*=\s*/);
  			return this._replaceVar(parts[0]) + '=' + this._interpretVars(parts[1]) + ';\n';
  		},
  		/**
  		 * Execute and output the results of a JavaScript expression, substituting variables if needed
  		 * 
  		 * @example - do division
  		 *   {$numTests} tests executed in {$execTime} seconds: average of {echo Math.round($numTests / $execTime, 2)} seconds per iteration
  		 */			
  		'echo': function(expression) {
  			return 'output += ' + this._interpretVars(expression) + ';\n';
  		},
  		/**
  		 * Specify javascript to be run; it will be in the scope of the instance and have access to "output";
  		 * 
  		 * @example - observe node
  		 *   <p id="myp3">When clicked, you should see a hello alert</p>{script delay="20"}$("myp3").onclick = function() { alert("hello"); }{/script}
  		 *   
       * @example - directly alter output
       *   <p>He has {script} output += ($mice > 9 ? 'too many' : 'just enough');{/script} mice</p>
  		 */			
  		'script': function(expression) {
  		  this.isInEval = true;
  		  var match = (/delay\s*\=\s*("|'|)(\d+)\1/).exec(expression);
  		  if (match) {
  		    this.delayed = match[2];
  		    return 'window.setTimeout(function() {\nvar output = "";\n';
  		  } else {
  		    return '';
  		  }
  		},
  		'/script': function() {
  		  var ret = '';
  		  if (this.delayed !== undefined) {
  		    ret = '}, ' + this.delayed + ')';
  		    delete this.delayed;
  		  }
  		  this.isInEval = false;
  			return ret;
  		},
  		/**
  		 * Output a series of <option> tags with one or more options selected
  		 * 
  		 * @example - do division
  		 *   
  		 */		
  		'options': function(expression) {
  			var params = this._extractParams(expression);
  			return '(function(values, currOutput, options, selected) {\n' +
  				tpl._helpRadioCheckboxOptions('selected') + 
  				'output += \'<option value="\' + $p.escapeHTML(value) + \'"\' + sel + \'>\' + $p.escapeHTML(options[value]) + \'</option>\\n\';\n' +
  				'} })(' + this._interpretVars(params.values || 'null') + ',' + this._interpretVars(params.currOutput || 'null') + ',' + this._interpretVars(params.options || 'null') + ',' + this._interpretVars(params.selected || '[]') + ');\n'
  			;
  		},
  		'radios': function(expression) {
  			return tpl._helpRadioCheckbox.call(this, 'radio', expression);
  		},
  		'checkboxes': function(expression) {
  			return tpl._helpRadioCheckbox.call(this, 'checkbox', expression);
  		}
  	}
  });

  // some browsers (such as IE) don't allow you to capture the split delimiter
  //   so create a generic function to split a string and capture the split tokens
  if ('abc'.split(/(b)/)[1] != 'b') {
    /** @ignore */
    tpl._splitAndCapture = function(subject, splitter) {
      var stack = [], match;
      while (subject.length > 0) {
        if (match = subject.match(splitter)) {
          stack.push(subject.slice(0, match.index));
          stack.push(subject.slice(match.index, match.index + match[0].length));
          subject = subject.slice(match.index + match[0].length);
        } else {
          stack.push(subject);
          subject = '';
        }
      }   
      return stack;
    }
  }
  		  
})();
