text: 'use strict';
  
  var Lexer = require('./lexer');
  var nodes = require('./nodes');
  var utils = require('./utils');
  var filters = require('./filters');
  var path = require('path');
  var constantinople = require('constantinople');
  var parseJSExpression = require('character-parser').parseMax;
  var extname = path.extname;
  
  /**
   * Initialize `Parser` with the given input `str` and `filename`.
   *
   * @param {String} str
   * @param {String} filename
   * @param {Object} options
   * @api public
   */
  
  var Parser = exports = module.exports = function Parser(str, filename, options){
    //Strip any UTF-8 BOM off of the start of `str`, if it exists.
    this.input = str.replace(/^\uFEFF/, '');
    this.lexer = new Lexer(this.input, filename);
    this.filename = filename;
    this.blocks = {};
    this.mixins = {};
    this.options = options;
    this.contexts = [this];
    this.inMixin = false;
    this.dependencies = [];
    this.inBlock = 0;
  };
  
  /**
   * Parser prototype.
   */
  
  Parser.prototype = {
  
    /**
     * Save original constructor
     */
  
    constructor: Parser,
  
    /**
     * Push `parser` onto the context stack,
     * or pop and return a `Parser`.
     */
  
    context: function(parser){
      if (parser) {
        this.contexts.push(parser);
      } else {
        return this.contexts.pop();
      }
    },
  
    /**
     * Return the next token object.
     *
     * @return {Object}
     * @api private
     */
  
    advance: function(){
      return this.lexer.advance();
    },
  
    /**
     * Single token lookahead.
     *
     * @return {Object}
     * @api private
     */
  
    peek: function() {
      return this.lookahead(1);
    },
  
    /**
     * Return lexer lineno.
     *
     * @return {Number}
     * @api private
     */
  
    line: function() {
      return this.lexer.lineno;
    },
  
    /**
     * `n` token lookahead.
     *
     * @param {Number} n
     * @return {Object}
     * @api private
     */
  
    lookahead: function(n){
      return this.lexer.lookahead(n);
    },
  
    /**
     * Parse input returning a string of js for evaluation.
     *
     * @return {String}
     * @api public
     */
  
    parse: function(){
      var block = new nodes.Block, parser;
      block.line = 0;
      block.filename = this.filename;
  
      while ('eos' != this.peek().type) {
        if ('newline' == this.peek().type) {
          this.advance();
        } else {
          var next = this.peek();
          var expr = this.parseExpr();
          expr.filename = expr.filename || this.filename;
          expr.line = next.line;
          block.push(expr);
        }
      }
  
      if (parser = this.extending) {
        this.context(parser);
        var ast = parser.parse();
        this.context();
  
        // hoist mixins
        for (var name in this.mixins)
          ast.unshift(this.mixins[name]);
        return ast;
      }
  
      if (!this.extending && !this.included && Object.keys(this.blocks).length){
        var blocks = [];
        utils.walkAST(block, function (node) {
          if (node.type === 'Block' && node.name) {
            blocks.push(node.name);
          }
        });
        Object.keys(this.blocks).forEach(function (name) {
          if (blocks.indexOf(name) === -1 && !this.blocks[name].isSubBlock) {
            console.warn('Warning: Unexpected block "'
                         + name
                         + '" '
                         + ' on line '
                         + this.blocks[name].line
                         + ' of '
                         + (this.blocks[name].filename)
                         + '. This block is never used. This warning will be an error in v2.0.0');
          }
        }.bind(this));
      }
  
      return block;
    },
  
    /**
     * Expect the given type, or throw an exception.
     *
     * @param {String} type
     * @api private
     */
  
    expect: function(type){
      if (this.peek().type === type) {
        return this.advance();
      } else {
        throw new Error('expected "' + type + '", but got "' + this.peek().type + '"');
      }
    },
  
    /**
     * Accept the given `type`.
     *
     * @param {String} type
     * @api private
     */
  
    accept: function(type){
      if (this.peek().type === type) {
        return this.advance();
      }
    },
  
    /**
     *   tag
     * | doctype
     * | mixin
     * | include
     * | filter
     * | comment
     * | text
     * | each
     * | code
     * | yield
     * | id
     * | class
     * | interpolation
     */
  
    parseExpr: function(){
      switch (this.peek().type) {
        case 'tag':
          return this.parseTag();
        case 'mixin':
          return this.parseMixin();
        case 'block':
          return this.parseBlock();
        case 'mixin-block':
          return this.parseMixinBlock();
        case 'case':
          return this.parseCase();
        case 'extends':
          return this.parseExtends();
        case 'include':
          return this.parseInclude();
        case 'doctype':
          return this.parseDoctype();
        case 'filter':
          return this.parseFilter();
        case 'comment':
          return this.parseComment();
        case 'text':
          return this.parseText();
        case 'each':
          return this.parseEach();
        case 'code':
          return this.parseCode();
        case 'call':
          return this.parseCall();
        case 'interpolation':
          return this.parseInterpolation();
        case 'yield':
          this.advance();
          var block = new nodes.Block;
          block.yield = true;
          return block;
        case 'id':
        case 'class':
          var tok = this.advance();
          this.lexer.defer(this.lexer.tok('tag', 'div'));
          this.lexer.defer(tok);
          return this.parseExpr();
        default:
          throw new Error('unexpected token "' + this.peek().type + '"');
      }
    },
  
    /**
     * Text
     */
  
    parseText: function(){
      var tok = this.expect('text');
      var tokens = this.parseInlineTagsInText(tok.val);
      if (tokens.length === 1) return tokens[0];
      var node = new nodes.Block;
      for (var i = 0; i < tokens.length; i++) {
        node.push(tokens[i]);
      };
      return node;
    },
  
    /**
     *   ':' expr
     * | block
     */
  
    parseBlockExpansion: function(){
      if (':' == this.peek().type) {
        this.advance();
        return new nodes.Block(this.parseExpr());
      } else {
        return this.block();
      }
    },
  
    /**
     * case
     */
  
    parseCase: function(){
      var val = this.expect('case').val;
      var node = new nodes.Case(val);
      node.line = this.line();
  
      var block = new nodes.Block;
      block.line = this.line();
      block.filename = this.filename;
      this.expect('indent');
      while ('outdent' != this.peek().type) {
        switch (this.peek().type) {
          case 'newline':
            this.advance();
            break;
          case 'when':
            block.push(this.parseWhen());
            break;
          case 'default':
            block.push(this.parseDefault());
            break;
          default:
            throw new Error('Unexpected token "' + this.peek().type
                            + '", expected "when", "default" or "newline"');
        }
      }
      this.expect('outdent');
  
      node.block = block;
  
      return node;
    },
  
    /**
     * when
     */
  
    parseWhen: function(){
      var val = this.expect('when').val;
      if (this.peek().type !== 'newline')
        return new nodes.Case.When(val, this.parseBlockExpansion());
      else
        return new nodes.Case.When(val);
    },
  
    /**
     * default
     */
  
    parseDefault: function(){
      this.expect('default');
      return new nodes.Case.When('default', this.parseBlockExpansion());
    },
  
    /**
     * code
     */
  
    parseCode: function(afterIf){
      var tok = this.expect('code');
      var node = new nodes.Code(tok.val, tok.buffer, tok.escape);
      var block;
      node.line = this.line();
  
      // throw an error if an else does not have an if
      if (tok.isElse && !tok.hasIf) {
        throw new Error('Unexpected else without if');
      }
  
      // handle block
      block = 'indent' == this.peek().type;
      if (block) {
        node.block = this.block();
      }
  
      // handle missing block
      if (tok.requiresBlock && !block) {
        node.block = new nodes.Block();
      }
  
      // mark presense of if for future elses
      if (tok.isIf && this.peek().isElse) {
        this.peek().hasIf = true;
      } else if (tok.isIf && this.peek().type === 'newline' && this.lookahead(2).isElse) {
        this.lookahead(2).hasIf = true;
      }
  
      return node;
    },
  
    /**
     * comment
     */
  
    parseComment: function(){
      var tok = this.expect('comment');
      var node;
  
      var block;
      if (block = this.parseTextBlock()) {
        node = new nodes.BlockComment(tok.val, block, tok.buffer);
      } else {
        node = new nodes.Comment(tok.val, tok.buffer);
      }
  
      node.line = this.line();
      return node;
    },
  
    /**
     * doctype
     */
  
    parseDoctype: function(){
      var tok = this.expect('doctype');
      var node = new nodes.Doctype(tok.val);
      node.line = this.line();
      return node;
    },
  
    /**
     * filter attrs? text-block
     */
  
    parseFilter: function(){
      var tok = this.expect('filter');
      var attrs = this.accept('attrs');
      var block;
  
      block = this.parseTextBlock() || new nodes.Block();
  
      var options = {};
      if (attrs) {
        attrs.attrs.forEach(function (attribute) {
          options[attribute.name] = constantinople.toConstant(attribute.val);
        });
      }
  
      var node = new nodes.Filter(tok.val, block, options);
      node.line = this.line();
      return node;
    },
  
    /**
     * each block
     */
  
    parseEach: function(){
      var tok = this.expect('each');
      var node = new nodes.Each(tok.code, tok.val, tok.key);
      node.line = this.line();
      node.block = this.block();
      if (this.peek().type == 'code' && this.peek().val == 'else') {
        this.advance();
        node.alternative = this.block();
      }
      return node;
    },
  
    /**
     * Resolves a path relative to the template for use in
     * includes and extends
     *
     * @param {String}  path
     * @param {String}  purpose  Used in error messages.
     * @return {String}
     * @api private
     */
  
    resolvePath: function (path, purpose) {
      var p = require('path');
      var dirname = p.dirname;
      var basename = p.basename;
      var join = p.join;
  
      if (path[0] !== '/' && !this.filename)
        throw new Error('the "filename" option is required to use "' + purpose + '" with "relative" paths');
  
      if (path[0] === '/' && !this.options.basedir)
        throw new Error('the "basedir" option is required to use "' + purpose + '" with "absolute" paths');
  
      path = join(path[0] === '/' ? this.options.basedir : dirname(this.filename), path);
  
      if (basename(path).indexOf('.') === -1) path += '.jade';
  
      return path;
    },
  
    /**
     * 'extends' name
     */
  
    parseExtends: function(){
      var fs = require('fs');
  
      var path = this.resolvePath(this.expect('extends').val.trim(), 'extends');
      if ('.jade' != path.substr(-5)) path += '.jade';
  
      this.dependencies.push(path);
      var str = fs.readFileSync(path, 'utf8');
      var parser = new this.constructor(str, path, this.options);
      parser.dependencies = this.dependencies;
  
      parser.blocks = this.blocks;
      parser.contexts = this.contexts;
      this.extending = parser;
  
      // TODO: null node
      return new nodes.Literal('');
    },
  
    /**
     * 'block' name block
     */
  
    parseBlock: function(){
      var block = this.expect('block');
      var mode = block.mode;
      var name = block.val.trim();
  
      var line = block.line;
  
      this.inBlock++;
      block = 'indent' == this.peek().type
        ? this.block()
        : new nodes.Block(new nodes.Literal(''));
      this.inBlock--;
      block.name = name;
      block.line = line;
  
      var prev = this.blocks[name] || {prepended: [], appended: []}
      if (prev.mode === 'replace') return this.blocks[name] = prev;
  
      var allNodes = prev.prepended.concat(block.nodes).concat(prev.appended);
  
      switch (mode) {
        case 'append':
          prev.appended = prev.parser === this ?
                          prev.appended.concat(block.nodes) :
                          block.nodes.concat(prev.appended);
          break;
        case 'prepend':
          prev.prepended = prev.parser === this ?
                           block.nodes.concat(prev.prepended) :
                           prev.prepended.concat(block.nodes);
          break;
      }
      block.nodes = allNodes;
      block.appended = prev.appended;
      block.prepended = prev.prepended;
      block.mode = mode;
      block.parser = this;
  
      block.isSubBlock = this.inBlock > 0;
  
      return this.blocks[name] = block;
    },
  
    parseMixinBlock: function () {
      var block = this.expect('mixin-block');
      if (!this.inMixin) {
        throw new Error('Anonymous blocks are not allowed unless they are part of a mixin.');
      }
      return new nodes.MixinBlock();
    },
  
    /**
     * include block?
     */
  
    parseInclude: function(){
      var fs = require('fs');
      var tok = this.expect('include');
  
      var path = this.resolvePath(tok.val.trim(), 'include');
      this.dependencies.push(path);
      // has-filter
      if (tok.filter) {
        var str = fs.readFileSync(path, 'utf8').replace(/\r/g, '');
        var options = {filename: path};
        if (tok.attrs) {
          tok.attrs.attrs.forEach(function (attribute) {
            options[attribute.name] = constantinople.toConstant(attribute.val);
          });
        }
        str = filters(tok.filter, str, options);
        return new nodes.Literal(str);
      }
  
      // non-jade
      if ('.jade' != path.substr(-5)) {
        var str = fs.readFileSync(path, 'utf8').replace(/\r/g, '');
        return new nodes.Literal(str);
      }
  
      var str = fs.readFileSync(path, 'utf8');
      var parser = new this.constructor(str, path, this.options);
      parser.dependencies = this.dependencies;
  
      parser.blocks = utils.merge({}, this.blocks);
      parser.included = true;
  
      parser.mixins = this.mixins;
  
      this.context(parser);
      var ast = parser.parse();
      this.context();
      ast.filename = path;
  
      if ('indent' == this.peek().type) {
        ast.includeBlock().push(this.block());
      }
  
      return ast;
    },
  
    /**
     * call ident block
     */
  
    parseCall: function(){
      var tok = this.expect('call');
      var name = tok.val;
      var args = tok.args;
      var mixin = new nodes.Mixin(name, args, new nodes.Block, true);
  
      this.tag(mixin);
      if (mixin.code) {
        mixin.block.push(mixin.code);
        mixin.code = null;
      }
      if (mixin.block.isEmpty()) mixin.block = null;
      return mixin;
    },
  
    /**
     * mixin block
     */
  
    parseMixin: function(){
      var tok = this.expect('mixin');
      var name = tok.val;
      var args = tok.args;
      var mixin;
  
      // definition
      if ('indent' == this.peek().type) {
        this.inMixin = true;
        mixin = new nodes.Mixin(name, args, this.block(), false);
        this.mixins[name] = mixin;
        this.inMixin = false;
        return mixin;
      // call
      } else {
        return new nodes.Mixin(name, args, null, true);
      }
    },
  
    parseInlineTagsInText: function (str) {
      var line = this.line();
  
      var match = /(\\)?#\[((?:.|\n)*)$/.exec(str);
      if (match) {
        if (match[1]) { // escape
          var text = new nodes.Text(str.substr(0, match.index) + '#[');
          text.line = line;
          var rest = this.parseInlineTagsInText(match[2]);
          if (rest[0].type === 'Text') {
            text.val += rest[0].val;
            rest.shift();
          }
          return [text].concat(rest);
        } else {
          var text = new nodes.Text(str.substr(0, match.index));
          text.line = line;
          var buffer = [text];
          var rest = match[2];
          var range = parseJSExpression(rest);
          var inner = new Parser(range.src, this.filename, this.options);
          buffer.push(inner.parse());
          return buffer.concat(this.parseInlineTagsInText(rest.substr(range.end + 1)));
        }
      } else {
        var text = new nodes.Text(str);
        text.line = line;
        return [text];
      }
    },
  
    /**
     * indent (text | newline)* outdent
     */
  
    parseTextBlock: function(){
      var block = new nodes.Block;
      block.line = this.line();
      var body = this.peek();
      if (body.type !== 'pipeless-text') return;
      this.advance();
      block.nodes = body.val.reduce(function (accumulator, text) {
        return accumulator.concat(this.parseInlineTagsInText(text));
      }.bind(this), []);
      return block;
    },
  
    /**
     * indent expr* outdent
     */
  
    block: function(){
      var block = new nodes.Block;
      block.line = this.line();
      block.filename = this.filename;
      this.expect('indent');
      while ('outdent' != this.peek().type) {
        if ('newline' == this.peek().type) {
          this.advance();
        } else {
          var expr = this.parseExpr();
          expr.filename = this.filename;
          block.push(expr);
        }
      }
      this.expect('outdent');
      return block;
    },
  
    /**
     * interpolation (attrs | class | id)* (text | code | ':')? newline* block?
     */
  
    parseInterpolation: function(){
      var tok = this.advance();
      var tag = new nodes.Tag(tok.val);
      tag.buffer = true;
      return this.tag(tag);
    },
  
    /**
     * tag (attrs | class | id)* (text | code | ':')? newline* block?
     */
  
    parseTag: function(){
      var tok = this.advance();
      var tag = new nodes.Tag(tok.val);
  
      tag.selfClosing = tok.selfClosing;
  
      return this.tag(tag);
    },
  
    /**
     * Parse tag.
     */
  
    tag: function(tag){
      tag.line = this.line();
  
      var seenAttrs = false;
      // (attrs | class | id)*
      out:
        while (true) {
          switch (this.peek().type) {
            case 'id':
            case 'class':
              var tok = this.advance();
              tag.setAttribute(tok.type, "'" + tok.val + "'");
              continue;
            case 'attrs':
              if (seenAttrs) {
                console.warn(this.filename + ', line ' + this.peek().line + ':\nYou should not have jade tags with multiple attributes.');
              }
              seenAttrs = true;
              var tok = this.advance();
              var attrs = tok.attrs;
  
              if (tok.selfClosing) tag.selfClosing = true;
  
              for (var i = 0; i < attrs.length; i++) {
                tag.setAttribute(attrs[i].name, attrs[i].val, attrs[i].escaped);
              }
              continue;
            case '&attributes':
              var tok = this.advance();
              tag.addAttributes(tok.val);
              break;
            default:
              break out;
          }
        }
  
      // check immediate '.'
      if ('dot' == this.peek().type) {
        tag.textOnly = true;
        this.advance();
      }
  
      // (text | code | ':')?
      switch (this.peek().type) {
        case 'text':
          tag.block.push(this.parseText());
          break;
        case 'code':
          tag.code = this.parseCode();
          break;
        case ':':
          this.advance();
          tag.block = new nodes.Block;
          tag.block.push(this.parseExpr());
          break;
        case 'newline':
        case 'indent':
        case 'outdent':
        case 'eos':
        case 'pipeless-text':
          break;
        default:
          throw new Error('Unexpected token `' + this.peek().type + '` expected `text`, `code`, `:`, `newline` or `eos`')
      }
  
      // newline*
      while ('newline' == this.peek().type) this.advance();
  
      // block?
      if (tag.textOnly) {
        tag.block = this.parseTextBlock();
      } else if ('indent' == this.peek().type) {
        var block = this.block();
        for (var i = 0, len = block.nodes.length; i < len; ++i) {
          tag.block.push(block.nodes[i]);
        }
      }
  
      return tag;
    }
  };
  