/* ***** BEGIN LICENSE BLOCK *****
 * The MIT License
 * 
 * Copyright (c) 2007 Mozilla Foundation
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 * ***** END LICENSE BLOCK ***** */

var _ast, _c;

/**
 * Implement L20n
 */

var l20n = {};
l20n.parser = {
  parse: function _parse(content) {
    var p = new l20n.parser.Parser();
    var ast = p.parseContent(content);
    var ap = new l20n.parser.ASTParser();
    return ap.getLOL(ast);
  },
  parseString: function _parseString(content) {
    var p = new l20n.parser.Parser();
    p._content = '"' + content + '"';
    p.offset = 0;
    var ast = p.getString();
    _ast = ast;
    var ap = new l20n.parser.ASTParser();
    return ap.getValue(ast);
  }
};

l20n.parser.ASTNode = function(type) {
  this.children = [];
  this.type = type;
};
l20n.parser.ASTNode.prototype = {
  push: function(child) {
    this.children.push(child);
  },
  toString: function() {
    var res = this.type + ':< ';
    var i;
    var cstrings = new Array(this.children.length);
    for (i = 0; i < this.children.length; ++i) {
      cstrings[i] = this.children[i];
    }
    res += cstrings.join(', ') + '>';
    return res;
  }
};
l20n.parser.ASTLeaf = function(type, value) {
  this.type = type;
  this.value = value;
};
l20n.parser.ASTLeaf.prototype = {
  toString: function() {
    return this.value;
  }
};

/**
 * Parser
 *
 * This object parses a string and returns an Abstract Syntax Tree (AST).
 * This code should be pretty language independent and be a guideline
 * on how to set up a parser.
 * ASTParser below maps the AST to the actual data structures used
 * to represent the data in this implementation.
 */
l20n.parser.Parser = function() {
};

l20n.parser.Parser.prototype = {
  /**
   * Public API
   */
  parseContent: function(content) {
    this._content = content;
    this._offset = 0;
    return this.getLOL();
  },
  /**
   * Data structures
   */
  _content : null,
  _offset : 0,
  /**
   * Internal API
   */
  /**
   * lol: WS? entry* EOF ;
   */
  getLOL: function _getLOL() {
    var lol, entry;
    lol = new l20n.parser.ASTNode("lol");
    this.getWS();
    while (this._content) {
      entry = this.getEntry();
      lol.push(entry);
    }
    return lol;
  },
  /**
   * entry: (entity | comment | group) WS?
   */
  getEntry: function _getEntry() {
    var entry = null;
    var lookahead = this._content.substr(0,3);
    var m = /(^<)|(^\/\*)|(\[%%)/.exec(lookahead);
    if (!m) {
      throw this.getExpectedException('"<" or "/*" or "[%%"');
    }
    var found = m.shift(); // drop the match
    switch(m.indexOf(found)) {
      case 0:
        entry = this.getEntity();
        break;
      case 1:
        entry = this.getComment();
        break;
      case 2:
        entry = this.getGroup();
        break;
      default:
        throw "Unexpected in getEntry";
    }
    this.getWS();
    return entry;
  },
  /**
   * entity: '<' ID WS? ( index WS? )? ':' WS?
   *         macro | value WS?
   *         ( keyValuePair WS? )*
   *         '>' ;
   */
  getEntity: function _getEntity() {
    var entity, ID, index, value, keyValuePair, m;
    entity = new l20n.parser.ASTNode('entity');
    m = /^</.exec(this._content);
    if (!m) {
      throw "Internal error in getEntity, '<' not found";
    }
    this.forward(m);
    ID = this.getID();
    entity.push(ID);
    this.getWS();
    var lookahead = this._content[0];
    if (lookahead == '[') {
      index = this.getIndex();
      entity.push(index);
      this.getWS();
    }
    m = /^:/.exec(this._content);
    if (!m) {
      throw this.getExpectedException(":");
    }
    this.forward(m);
    this.getWS();
    // macro lookahead
    m = /^\(/.exec(this._content);
    if (m) {
      value = this.getMacro();
    }
    else {
      value = this.getValue();
    }
    entity.push(value);
    this.getWS();
    m = /^>/.exec(this._content);
    while (!m) {
      keyValuePair = this.getKeyValuePair();
      entity.push(keyValuePair);
      this.getWS();
      m = /^>/.exec(this._content);
    }
    this.forward(m);
    return entity;
  },
  /**
   * ID: \w+ ;
   */
  getID: function _getID() {
    var m;
    m = /^\w+/.exec(this._content);
    if (!m) {
      throw this.getExpectedException('letter');
    }
    this.forward(m);
    return new l20n.parser.ASTLeaf("ID", m[0]);
  },
  /**
   * index: '[' WS? expression WS? ( ',' WS? expression WS? )*']' ;
   */
  getIndex: function _getIndex() {
    var index, expression, m;
    index = new l20n.parser.ASTNode('index');
    m = /^\[/.exec(this._content);
    if (!m) {
      throw "Internal error, [ expected";
    }
    this.forward(m);
    this.getWS();
    expression = this.getExpression();
    index.push(expression);
    this.getWS();
    m = /^,/.exec(this._content);
    while (m) {
      this.forward(m);
      this.getWS();
      expression = this.getExpression();
      index.push(expression);
      this.getWS();
      m = /^,/.exec(this._content);
    }
    m = /^]/.exec(this._content);
    if (!m) {
      throw this.getExpectedException(']');
    }
    this.forward(m);
    return index;
  },
  /**
   * expression : conditional_expression ;
   */
  getExpression: function _getExpression() {
    return this.getConditionalExpression();
  },
  /**
   * conditional_expression : or_expression WS? ( '?' WS? expression ':' WS? conditional_expression WS? )? ;
   */
  getConditionalExpression: function _getConditionalExpression() {
    var conditional_expression, or_expression, expression, conditional_expression2, m;
    or_expression = this.getOrExpression();
    this.getWS();
    m = /^\?/.exec(this._content);
    if (!m) {
      return or_expression;
    }
    conditional_expression = new l20n.parser.ASTNode('conditional_expression');
    conditional_expression.push(or_expression);
    this.forward(m);
    this.getWS();
    expression = this.getExpression();
    conditional_expression.push(expression);
    this.getWS();
    m = /^:/.exec(this._content);
    if (!m) {
      thow.this.getExpectedException(':');
    }
    this.forward(m);
    this.getWS();
    conditional_expression2 = this.getConditionalExpression();
    conditional_expression.push(conditional_expression2);
    this.getWS();
    return conditional_expression;
  },
  /**
   * or_expression: and_expression WS? ( '||' WS? and_expression WS? )* ;
   */
  getOrExpression: function _getOrExpression() {
    var or_expression, and_expression, m;
    and_expression = this.getAndExpression();
    this.getWS();
    m = /^\|\|/.exec(this._content);
    if (!m) {
      return and_expression;
    }
    or_expression = new l20n.parser.ASTNode("or_expression");
    or_expression.push(and_expression);
    while (m) {
      this.forward(m);
      this.getWS();
      and_expression = this.getAndExpression();
      or_expression.push(and_expression);
      this.getWS();
      m = /^\|\|/.exec(this._content);
    }
    return or_expression;
  },
  /**
   * and_expression: equality_expression WS? ( '&&' WS? equality_expression WS? )* ;
   */
  getAndExpression: function _getAndExpression() {
    var and_expression, equality_expression, m;
    equality_expression = this.getEqualityExpression();
    this.getWS();
    m = /^\&\&/.exec(this._content);
    if (!m) {
      return equality_expression;
    }
    and_expression = new l20n.parser.ASTNode("and_expression");
    and_expression.push(equality_expression);
    while (m) {
      this.forward(m);
      this.getWS();
      equality_expression = this.getEqualityExpression();
      and_expression.push(equality_expression);
      this.getWS();
      m = /^\&\&/.exec(this._content);
    }
    return and_expression;
  },
  /**
   * equality_expression:  relational_expression WS? ( ('=='|'!=') WS? relational_expression WS? )* ;
   */
  getEqualityExpression: function _getEqualityExpression() {
    var equality_expression, relational_expression, m;
    relational_expression = this.getRelationalExpression();
    this.getWS();
    m = /^[!=]=/.exec(this._content);
    if (!m) {
      return relational_expression;
    }
    equality_expression = new l20n.parser.ASTNode("equality_expression");
    equality_expression.push(relational_expression);
    while (m) {
      equality_expression.push(new l20n.parser.ASTLeaf("equality_op", m[0]));
      this.forward(m);
      this.getWS();
      relational_expression = this.getRelationalExpression();
      equality_expression.push(relational_expression);
      this.getWS();
      m = /^[!=]=/.exec(this._content);
    }
    return equality_expression;
  },
  /**
   * relational_expression: additive_expression WS? ( ('<'|'>'|'<='|'>=') WS? additive_expression WS? )* ;
   */
  getRelationalExpression: function _getRelationalExpression() {
    var relational_expression, additive_expression, m;
    additive_expression = this.getAdditiveExpression();
    this.getWS();
    m = /^[<>]=?/.exec(this._content);
    if (!m) {
      return additive_expression;
    }
    relational_expression = new l20n.parser.ASTNode("relational_expression");
    relational_expression.push(additive_expression);
    while (m) {
      relational_expression.push(new l20n.parser.ASTLeaf("relational_op", m[0]));
      this.forward(m);
      this.getWS();
      additive_expression = this.getAdditiveExpression();
      relational_expression.push(additive_expression);
      this.getWS();
      m = /^[<>]=?/.exec(this._content);
    }
    return relational_expression;
  },
  /**
   * additive_expression: multiplicative_expression WS? ( ('+' | '-') WS? multiplicative_expression WS? )* ;
   */
  getAdditiveExpression: function _getAdditiveExpression() {
    var additive_expression, multiplicative_expression, m;
    multiplicative_expression = this.getMultiplicativeExpression();
    this.getWS();
    m = /^[\+\-]/.exec(this._content);
    if (!m) {
      return multiplicative_expression;
    }
    additive_expression = new l20n.parser.ASTNode("additive_expression");
    additive_expression.push(multiplicative_expression);
    while (m) {
      additive_expression.push(new l20n.parser.ASTLeaf("additive_op", m[0]));
      this.forward(m);
      this.getWS();
      multiplicative_expression = this.getMultiplicativeExpression();
      additive_expression.push(multiplicative_expression);
      this.getWS();
      m = /^[\+\-]/.exec(this._content);
    }
    return additive_expression;
  },
  /**
   * multiplicative_expression: unary_expression WS? ( ('*' | '/' | '%') WS? unary_expression WS? )* ;
   */
  getMultiplicativeExpression: function _getMultiplicativeExpression() {
    var multiplicative_expression, unary_expression, m;
    unary_expression = this.getUnaryExpression();
    this.getWS();
    m = /^[\*\/\%]/.exec(this._content);
    if (!m) {
      return unary_expression;
    }
    multiplicative_expression = new l20n.parser.ASTNode("multiplicative_expression");
    multiplicative_expression.push(unary_expression);
    while (m) {
      multiplicative_expression.push(new l20n.parser.ASTLeaf("multiplicative_op", m[0]));
      this.forward(m);
      this.getWS();
      unary_expression = this.getUnaryExpression();
      multiplicative_expression.push(unary_expression);
      this.getWS();
      m = /^[\*\/\%]/.exec(this._content);
    }
    return multiplicative_expression;
  },
  /**
   * unary_expression: ( ('+' | '-' | '!') WS? unary_expression ) | postfix_expression ;
   */
  getUnaryExpression: function _getUnaryExpression() {
    var unary_expression, unary_expression2, m;
    m = /^[\+\-\!]/.exec(this._content);
    if (!m) {
      return this.getPostfixExpression();
    }
    unary_expression = new l20n.parser.ASTNode("unary_expression");
    unary_expression.push(new l20n.parser.ASTLeaf("unary_op", m[0]));
    this.forward(m);
    this.getWS();
    unary_expression2 = this.getUnaryExpression();
    unary_expression.push(unary_expression2);
    return unary_expression;
  },
  /**
   * postfix_expression:
   */
  getPostfixExpression: function _getPostfixExpression() {
    return this.getPrimaryExpression();
  },
  /**
   * primary_expression: ( '(' WS? expression ')' ) | number | value |
   *   idref (
   *     ( '(' WS? ( expression WS? ( ',' WS? expression WS? )* )? ')' )
   *   | index
   *         )? ;
   */
  getPrimaryExpression: function _getPrimaryExpression() {
    var primary_expression, expression, value, idref, index, m;
    m = /^\(/.exec(this._content);
    if (m) {
      // maybe optimize this one out
      primary_expression = new l20n.parser.ASTNode("brace_expression");
      this.forward(m);
      this.getWS();
      expression = this.getExpression();
      primary_expression.push(expression);
      this.getWS();
      m = /^\)/.exec(this._content);
      if (!m) {
        throw this.getExpectedException(')');
      }
      this.forward(m);
      this.getWS();
      return primary_expression;
    }
    // number
    m = /^[0-9]+/.exec(this._content);
    if (m) {
      this.forward(m);
      this.getWS();
      return new l20n.parser.ASTLeaf("number", m[0]);
    }
    // lookahead for value
    m = /^[\"\'\[\{]/.exec(this._content);
    if (m) {
      return this.getValue();
    }
    // idref (with index?) or macrocall
    idref = this.getIdref();
    // check for index
    m = /^\[/.exec(this._content);
    if (m) {
      index = this.getIndex();
      idref.push(index);
      return idref;
    }
    m = /^\(/.exec(this._content);
    if (!m) {
      return idref;
    }
    primary_expression = new l20n.parser.ASTNode("macro_call");
    primary_expression.push(idref);
    this.forward(m);
    this.getWS();
    m = /^\)/.exec(this._content);
    if (!m) {
      // argument list
      expression = this.getExpression();
      primary_expression.push(expression);
      this.getWS();
      m = /^,/.exec(this._content);
      while (m) {
        this.forward(m);
        this.getWS();
        expression = this.getExpression();
        primary_expression.push(expression);
        this.getWS();
        m = /^,/.exec(this._content);
      }
    }
    m = /^\)/.exec(this._content);
    if (!m) {
      throw this.getExpectedException(')');
    }
    this.forward(m);
    this.getWS();
    return primary_expression;
  },
  /**
   * idref: ID ( '.' ID )* ;
   */
  getIdref: function _getIdref() {
    var idref, ID, m;
    idref = new l20n.parser.ASTNode('idref');
    ID = this.getID();
    idref.push(ID);
    m = /^\./.exec(this._content);
    while (m) {
      this.forward(m);
      ID = this.getID();
      idref.push(ID);
      m = /^\./.exec(this._content);
    }
    return idref;
  },
  /**
   * value: string | array | hash ;
   */
  getValue: function _getValue() {
    var value;
    var lookahead = this._content[0];
    switch (lookahead) {
      case '"':
      case "'":
        // get string
        value = this.getString();
        break;
      case '[':
        // get array
        value = this.getArray();
        break;
      case '{':
        // get hash
        value = this.getHash();
        break;
      default:
        throw this.getExpectedException("\"'[{");
    }
    return value;
  },
  /**
   * macro: '(' WS? ( ID WS? ( ',' WS? ID WS? )* )? ')' WS?
   *        '->' WS? '{' WS? expression WS? '}' ;
   */
  getMacro: function _getMacro() {
    var macro, idlist, id, expression, m;
    macro = new l20n.parser.ASTNode('macro');
    idlist = new l20n.parser.ASTNode('idlist');
    macro.push(idlist);
    m = /^\(/.exec(this._content);
    if (!m) {
      throw "Internal error, ( expected";
    }
    this.forward(m);
    this.getWS();
    m = /^\)/.exec(this._content);
    if (!m) {
      id = this.getID();
      idlist.push(id);
      this.getWS();
      m = /^,/.exec(this._content);
      while (m) {
        this.forward(m);
        this.getWS();
        id = this.getID();
        idlist.push(id);
        this.getWS();
        m = /^,/.exec(this._content);
      }
      m = /^\)/.exec(this._content);
    }
    if (!m) {
      throw this.getExpectedException(")");
    }
    this.forward(m);
    this.getWS();
    m = /^->/.exec(this._content);
    if (!m) {
      throw this.getExpectedException('->');
    }
    this.forward(m);
    this.getWS();
    m = /^\{/.exec(this._content);
    if (!m) {
      throw this.getExpectedException('{');
    }
    this.forward(m);
    this.getWS();
    expression = this.getExpression();
    macro.push(expression);
    this.getWS();
    m = /^\}/.exec(this._content);
    if (!m) {
      throw this.getExpectedException('}');
    }
    this.forward(m);
    return macro;
  },
  /**
   * string: '\'' ([^'] | escape | expander )* '\''
   * escape: '\' ('\' | '\'' | '"' | '$') ;
   * expander: '${' expression '}' ('s' | 'i') ;
   * ... or so ;-), this is hacked to return a simple string for
   * simple strings, and a complex one for complex ones, and do 
   * eat both ' and ".
   */
  getString: function _getString() {
    var string, m, expander, expression;
    string = new l20n.parser.ASTNode("complexString");
    m = /^(\'|\")/.exec(this._content);
    if (!m) {
      throw "Internal error, \" or ' expected";
    }
    this.forward(m);
    var str_end = new RegExp('^' + m[0]);
    var literal = new RegExp('^([^\\\\$' + m[0] + ']+)');
    m = str_end.exec(this._content);
    var buffer = '';
    while (!m && this._content) {
      // escape sequence
      m = /^\\/.exec(this._content);
      if (m) {
        // XXX warn about unknown escape?
        this.forward(m);
        buffer += this._content[0];
        this.forward(m); // just forward one char again
      }
      m = /^\$/.exec(this._content);
      if (m) {
        this.forward(m);
        if (this._content[0] != '{') {
          throw this.getExpectedException('{');
        }
        this.forward(m); // forward one more char, the '{'
        expander = new l20n.parser.ASTNode("expander");
        expression = this.getExpression();
        if (buffer) {
          string.push(new l20n.parser.ASTLeaf("string", buffer));
          buffer = '';
        }
        expander.push(expression);
        m = /^\}/.exec(this._content);
        if (!m) {
          throw this.getExpectedException('}');
        }
        this.forward(m);
        m = /^(i|s)/.exec(this._content);
        if (!m) {
          throw this.getExpectedException('i|s');
        }
        this.forward(m);
        expander.push(new l20n.parser.ASTLeaf("flag", m[1]));
        string.push(expander);
      }
      m = literal.exec(this._content);
      if (m) {
        buffer += m[1];
        this.forward(m);
      }
      m = str_end.exec(this._content);
    }
    if (!m) {
      throw this.getExpectedException('end of string');
    }
    this.forward(m);
    if (buffer || string.children.length == 0) {
      string.push(new l20n.parser.ASTLeaf("string", buffer));
    }
    if (string.children.length > 1 ||
        string.children[0].type == "expander") {
      return string;
    }
    return string.children[0];
  },
  /**
   * array: '[' WS? value WS? ( ',' WS? value WS? )* ']' ;
   */
  getArray: function _getArray() {
    var array = new l20n.parser.ASTNode("array");
    var m;
    var value;
    m = /^\[/.exec(this._content);
    if (!m) {
      throw "Internal error, [ not found";
    }
    this.forward(m);
    this.getWS();
    value = this.getValue();
    array.push(value);
    this.getWS();
    m = /^,/.exec(this._content);
    while (m) {
      this.forward(m);
      this.getWS();
      value = this.getValue();
      array.push(value);
      this.getWS();
      m = /^,/.exec(this._content);
    }
    m = /^]/.exec(this._content);
    if (!m) {
      throw this.getExpectedException("]");
    }
    this.forward(m);
    return array;
  },
  /**
   * hash: '{' WS? keyValuePair WS? ( ',' WS? keyValuePair WS? )* '}' ;
   */
  getHash: function _getHash() {
    var hash, keyValuePair, m;
    hash = new l20n.parser.ASTNode("hash");
    m = /^{/.exec(this._content);
    if (!m) {
      throw "Internal error, { not found";
    }
    this.forward(m);
    this.getWS();
    keyValuePair = this.getKeyValuePair();
    hash.push(keyValuePair);
    this.getWS();
    m = /^,/.exec(this._content);
    while (m) {
      this.forward(m);
      this.getWS();
      keyValuePair = this.getKeyValuePair();
      hash.push(keyValuePair);
      this.getWS();
      m = /^,/.exec(this._content);
    }
    m = /^}/.exec(this._content);
    if (!m) {
      throw this.getExpectedException("}");
    }
    this.forward(m);
    return hash;
  },
  /**
   * keyValuePair: ID WS? ':' WS? value ;
   */
  getKeyValuePair: function _getKeyValuePair() {
    var keyValuePair, ID, m, value;
    keyValuePair = new l20n.parser.ASTNode('keyValuePair');
    ID = this.getID();
    keyValuePair.push(ID);
    this.getWS();
    m = /^:/.exec(this._content);
    if (!m) {
      throw this.getExpectedException(':');
    }
    this.forward(m);
    this.getWS();
    value = this.getValue();
    keyValuePair.push(value);
    return keyValuePair;
  },
  /**
   * comment: '/' '*' .*? '*' '/' ;
   */
  getComment: function _getComment() {
    var m = /^\/\*/.exec(this._content);
    if (!m) {
      throw "Internal error in getComment, '/*' not found";
    }
    m = /^\/\*(?:\n|\r|.)*?\*\//.exec(this._content); // '.' is non-greedy
    if (!m) {
      throw this.getExpectedException("*/");
    }
    this.forward(m);
    return new l20n.parser.ASTLeaf('comment', m[0]);
  },
  /**
   * group: '[%%' WS? entry* '%%]' ;
   */
  getGroup: function _getGroup() {
    var group, entry, m;
    group = new l20n.parser.ASTNode("group");
    m = /^\[%%\s*/.exec(this._content); // skip whitespace right away
    if (!m) {
      throw "Internal error in getGroup, '[%%' not found";
    }
    this.forward(m);
    this.getWS();
    m = /^%%\]/.exec(this._content);
    while (!m) {
      entry = this.getEntry();
      group.push(entry);
      m = /^%%\]/.exec(this._content);
    }
    this.forward(m);
    return group;
  },
  /**
   * WhiteSpace skipping
   */
  getWS: function _gWS() {
    var m = /^\s+/.exec(this._content);
    if (!m) {
      return;
    }
    this.forward(m);
  },
  forward: function _f(match) {
    var string = match[0];
    this._content = this._content.substr(string.length);
    this._offset += string.length;
  },
  getExpectedException: function(tokens) {
    var msg = 'Expected ' + tokens + ' at offset ' + this._offset +
      ', ' + this._content.substr(0, 3);
    return new Error(msg);
  }
};

/**
 * AS Tree parser
 *
 * This parser transforms the AST into actual JS data structures
 * to be used by this implementation.
 */
l20n.parser.ASTParser = function() {};
l20n.parser.ASTParser.prototype = {
  getLOL: function _getLOL(AST) {
    if (!(AST instanceof l20n.parser.ASTNode) ||
      AST.type != "lol") {
      throw "Internal error, lol expected";
    }
    var lol, i;
    lol = new l20n.service.LOL();
    for (i = 0; i < AST.children.length; ++i) {
      var entry = AST.children[i];
      this.getEntry(entry, lol);
    }
    return lol;
  },
  getEntry: function _getEntry(AST, out) {
    if (AST instanceof l20n.parser.ASTLeaf) {
      if (AST.type != "comment") {
        throw "Internal error, top-level leaf entry is not a comment";
      }
      return; // don't do anything useful with comments
    }
    if (!(AST instanceof l20n.parser.ASTNode)) {
      throw "Internal error, huh? Neither Node nor Leaf?";
    }
    switch (AST.type) {
      case "group":
        // This is a tad hacky, it'd be nice if we wouldn't change
        // the input AST
        this.getGroup(AST, out);
        break;
      case "entity":
        this.getEntity(AST, out);
        break;
    }
  },
  getEntity: function _getEntity(AST, out) {
    // ASSERT?
    var entity = new l20n.service.Entity();
    var c = AST.children;
    var i = 0;
    // ASSERT(c[i].type == "ID");
    entity.ID = c[i].value;
    ++i;
    if (c[i].type == "index") {
      entity.index = this.getIndex(c[i]);
      ++i;
    }
    if (c[i].type == "macro") {
      entity.value = this.getMacro(c[i]);
    }
    else {
      entity.value = this.getValue(c[i]);
    }
    ++i;
    var key, value;
    for (; i < c.length; ++i) {
      // ASSERT(c[i].type == "keyValuePair");
      // ASSERT(c[i].children[0].type == "ID");
      key = c[i].children[0].value;
      value = this.getValue(c[i].children[1]);
      entity.attributes[key] = value;
    }
    out.addEntity(entity);
  },
  getIndex: function _getIndex(AST) {
    // ASSERT(AST.type == "index");
    var index, i;
    index = new Array(AST.children.length);
    for (i=0; i < AST.children.length; ++i) {
      index[i] = this.getExpression(AST.children[i]);
    }
    return index;
  },
  getExpression: function _getExpression(AST) {
    var expression;
    switch (AST.type) {
      case "conditional_expression":
        expression = new l20n.service.expressions.Conditional(AST, this);
        break;
      case "or_expression":
        expression = new l20n.service.expressions.OrExpression(AST, this);
        break;
      case "and_expression":
        expression = new l20n.service.expressions.AndExpression(AST, this);
        break;
      case "equality_expression":
        expression = new l20n.service.expressions.EqualityExpression(AST, this);
        break;
      case "relational_expression":
        expression = new l20n.service.expressions.RelationalExpression(AST, this);
        break;
      case "additive_expression":
        expression = new l20n.service.expressions.AdditiveExpression(AST, this);
        break;
      case "multiplicative_expression":
        expression = new l20n.service.expressions.MultiplicativeExpression(AST, this);
        break;
      case "unary_expression":
        expression = new l20n.service.expressions.UnaryExpression(AST, this);
        break;
      case "brace_expression":
        expression = new l20n.service.expressions.BraceExpression(AST, this);
        break;
      case "idref":
        expression = new l20n.service.expressions.Idref(AST, this);
        break;
      case "macro_call":
        expression = new l20n.service.expressions.MacroExpression(AST, this);
        break;
      case "number":
      case "string":
      case "complexString":
      case "array":
      case "hash":
        expression  = this.getValue(AST);
      default:
        // ASSERT(0, "Shouldn't happen");
    }
    return expression;
  },
  getMacro: function _getMacro(AST) {
    var macro;
    macro = new l20n.service.Macro(AST, this);
    return macro;
  },
  getValue: function _getValue(AST) {
    if (AST.type == "string") {
      return AST.value;
    }
    if (AST.type == "number") {
      return Number(AST.value).valueOf();
    }
    var i, c;
    if (AST.type == "complexString") {
      var complexString = new l20n.service.expressions.ComplexString();
      for (i=0; i < AST.children.length; ++i) {
        c = AST.children[i];
        if (c.type == "string") {
          complexString.push(c.value);
        }
        else {
          // ASSERT(c.type == "expander");
          // ASSERT(c.children[0].type == "idref");
          var expression = this.getExpression(c.children[0]);
          var flag = c.children[1];
          // ASSERT(flag.type == "flag");
          complexString.push(expression);
        }
      }
      return complexString;
    }
    if (AST.type == "array") {
      var array = new Array(AST.children.length);
      for (i=0; i < AST.children.length; ++i) {
        array[i] = this.getValue(AST.children[i]);
      }
      return array;
    }
    // ASSERT(AST.type == "hash");
    var key, value, hash = {};
    for (i=0; i < AST.children.length; ++i) {
      c = AST.children[i];
      // ASSERT(c.type == "keyValuePair");
      // ASSERT(c.children[0].type == "ID");
      key = c.children[0].value;
      value = this.getValue(c.children[1]);
      hash[key] = value;
    }
    return hash;
  },
  getGroup: function _getGroup(AST, out) {
    var i;
    for (i=0; i < AST.children.length; ++i) {
      this.getEntry(AST.children[i], out);
    }
  }
};

l20n.service = {
  getLOL: function _getLOL(aRef, aLocale, aContext) {
    var cachekey = [aLocale, aRef].toSource();
    var srv = this;
    if (cachekey in this._cache) {
      aContext.addLOL(this._cache[cachekey]);
      function cb(){srv.notifyObservers();};
      window.setTimeout(cb, 0);
      return;
    }
    var ref = l20n.parser.parseString(aRef);
    if (ref instanceof l20n.service.expressions.ComplexString) {
      var c = {
        resolveEntity: function _get(key){return this._m[key];},
        _m: {_locale: aLocale},
        parent: null
      };
      ref = ref.valueOf(c);
    }
    var callback = function(type, data, evt) {
      var lol;
      try {
        lol = l20n.parser.parse(data);
      }
      catch (e) {
        LOG(e);
        return;
      }
      l20n.service._cache[cachekey] = lol;
      aContext.addLOL(lol);
      srv.notifyObservers();
    };
    this.loader.start(ref, callback);
  },
  addObserver: function _addObserver(o) {
    this._observers.push(o);
  },
  notifyObservers: function _notify() {
    var i;
    for (i = 0; i < this._observers.length; ++i) {
      this._observers[i].onLoad();
    }
  },
  resolveEntity: function _resolve(entity_ref, context) {
    var entity = null, c=context;
    while (!entity && c) {
      entity = c.resolveEntity(entity_ref);
      c = c.parent;
    }
    return entity;
  },
  evalWith: function _evalWith(expr, context) {
    if (typeof(expr) == "string") {
      return expr;
    }
    return expr.valueOf(context)
  },
  loader: null, // set dependent on present libraries
  _observers: [],
  _cache: {},
  LOL: function _lolConstructor() {
    this._map = {};
  },
  Context: function _contextConstructor(parent) {
    this._locale = null;
    this._lols = [];
    this._refs = [];
    this._parent = parent;
  },
  Entity: function _entityConstructor() {
    this.ID = null;
    this.index = null;
    this.value = null;
    this.attributes = {};
  },
  Macro: function _macroConstructor(AST, parser) {
    // ASSERT(AST.type == "idref");
    // ASSERT(AST.children.length == 2);
    // ASSERT(AST.children[0].type == "idlist");
    var i;
    this.params = new Array(AST.children[0].children.length);
    for (i = 0; i < AST.children[0].children.length; ++i) {
      this.params[i] = AST.children[0].children[i].value;
    }
    this.expression = parser.getExpression(AST.children[1]);
  },
  expressions: {
    Conditional: function _conditionalConstructor(AST, parser) {
      // ASSERT(AST.children.length == 3);
      this.or_expression = parser.getExpression(AST.children[0]);
      this.expression = parser.getExpression(AST.children[1]);
      this.conditional_expression = parser.getExpression(AST.children[2]);
      this.valueOf = function(context) {
        if (!context) {
          return "conditional_expression";
        }
        var condition = this.or_expression.valueOf(context);
        if (condition) {
          return this.expression.valueOf(context);
        }
        return this.conditional_expression.valueOf(context);
      };
    },
    OrExpression: function _orConstructor(AST, parser) {
      // ASSERT(AST.children.length >= 2);
      this.expressions = new Array(AST.children.length);
      var i;
      for (i = 0; i < AST.children.length; ++i) {
        this.expressions[i] = parser.getExpression(AST.children[i]);
      }
      this.valueOf = function(context) {
        if (!context) {
          return "or_expression";
        }
        var i, val;
        for (i = 0; i < this.expressions.length; ++i) {
          val = this.expressions[i].valueOf(context);
          if (val) {
            return val;
          }
        }
        return val;
      };
    },
    AndExpression: function _andConstructor(AST, parser) {
      // ASSERT(AST.children.length >= 2);
      this.expressions = new Array(AST.children.length);
      var i;
      for (i = 0; i < AST.children.length; ++i) {
        this.expressions[i] = parser.getExpression(AST.children[i]);
      }
      this.valueOf = function(context) {
        if (!context) {
          return "and_expression";
        }
        var i, val;
        for (i = 0; i < this.expressions.length; ++i) {
          val = this.expressions[i].valueOf(context);
          if (!val) {
            return val;
          }
        }
        return val;
      };
    },
    EqualityExpression: function _equalityConstructor(AST, parser) {
      // ASSERT(AST.children.length % 2 == 1);
      // ASSERT(AST.children.length> 2);
      this.expressions = new Array(AST.children.length);
      var i = 0;
      this.expressions[i] = parser.getExpression(AST.children[i]);
      for (i = 1; i < AST.children.length; ++i) {
        this.expressions[i] = AST.children[i].value == '==';
        ++i;
        this.expressions[i] = parser.getExpression(AST.children[i]);
      }
      this.valueOf = function(context) {
        if (!context) {
          return "equality_expression";
        }
        var i = 0, val, isEqual;
        val = this.expressions[i].valueOf(context);
        for (i = 1; i < this.expressions.length; ++i) {
          isEqual = this.expressions[i];
          ++i;
          if (isEqual) {
            val = val == this.expressions[i].valueOf(context);
          }
          else {
            val = val != this.expressions[i].valueOf(context);
          }
        }
        return val;
      };
    },
    RelationalExpression: function _relationalConstructor(AST, parser) {
      // ASSERT(AST.children.length % 2 == 1);
      // ASSERT(AST.children.length> 2);
      this.expressions = new Array(AST.children.length);
      var i = 0;
      this.expressions[i] = parser.getExpression(AST.children[i]);
      for (i = 1; i < AST.children.length; ++i) {
        this.expressions[i] = AST.children[i].value;
        ++i;
        this.expressions[i] = parser.getExpression(AST.children[i]);
      }
      this.valueOf = function(context) {
        if (!context) {
          return "relational_expression";
        }
        var i = 0, val, op;
        val = this.expressions[i].valueOf(context);
        for (i = 1; i < this.expressions.length; ++i) {
          op = this.expressions[i];
          ++i;
          switch (op) {
            case "<":
              val = val < this.expressions[i].valueOf(context);
              break;
            case "<=":
              val = val <= this.expressions[i].valueOf(context);
              break;
            case ">":
              val = val > this.expressions[i].valueOf(context);
              break;
            case ">=":
              val = val >= this.expressions[i].valueOf(context);
              break;
          }
        }
        return val;
      };
    },
    AdditiveExpression: function _additiveConstructor(AST, parser) {
      // ASSERT(AST.children.length % 2 == 1);
      // ASSERT(AST.children.length> 2);
      this.expressions = new Array(AST.children.length);
      var i = 0;
      this.expressions[i] = parser.getExpression(AST.children[i]);
      for (i = 1; i < AST.children.length; ++i) {
        this.expressions[i] = AST.children[i].value == '+';
        ++i;
        this.expressions[i] = parser.getExpression(AST.children[i]);
      }
      this.valueOf = function(context) {
        if (!context) {
          return "additive_expression";
        }
        var i = 0, val, isPlus;
        val = this.expressions[i].valueOf(context);
        for (i = 1; i < this.expressions.length; ++i) {
          isPlus = this.expressions[i];
          ++i;
          if (isPlus) {
            val = val + this.expressions[i].valueOf(context);
          }
          else {
            val = val - this.expressions[i].valueOf(context);
          }
        }
        return val;
      };
    },
    MultiplicativeExpression: function _multiplicativeConstructor(AST, parser) {
      // ASSERT(AST.children.length % 2 == 1);
      // ASSERT(AST.children.length> 2);
      this.expressions = new Array(AST.children.length);
      var i = 0;
      this.expressions[i] = parser.getExpression(AST.children[i]);
      for (i = 1; i < AST.children.length; ++i) {
        this.expressions[i] = AST.children[i].value;
        ++i;
        this.expressions[i] = parser.getExpression(AST.children[i]);
      }
      this.valueOf = function(context) {
        if (!context) {
          return "multiplicative_expression";
        }
        var i = 0, val, op;
        val = this.expressions[i].valueOf(context);
        for (i = 1; i < this.expressions.length; ++i) {
          op = this.expressions[i];
          ++i;
          switch (op) {
            case "*":
              val = val * this.expressions[i].valueOf(context);
              break;
            case "/":
              val = val / this.expressions[i].valueOf(context);
              break;
            case "%":
              val = val % this.expressions[i].valueOf(context);
              break;
          }
        }
        return val;
      };
    },
    UnaryExpression: function _unaryConstructor(AST, parser) {
      // ASSERT(AST.children.length == 2);
      this.expressions = new Array(AST.children.length);
      this.op = AST.children[0].value;
      this.expression = parser.getExpression(AST.children[1]);
      this.valueOf = function(context) {
        if (!context) {
          return "unary_expression";
        }
        var val;
        val = this.expression.valueOf(context);
        switch (this.op) {
          case "+":
            val = + val;
            break;
          case "-":
            val = - val;
            break;
          case "!":
            val = ! val;
            break;
        }
        return val;
      };
    },
    BraceExpression: function _braceConstructor(AST, parser) {
      // ASSERT(AST.children.length == 1);
      this.expression = parser.getExpression(AST.children[0]);
      this.valueOf = function(context) {
        if (!context) {
          return "brace_expression";
        }
        return this.expression.valueOf(context);
      }
    },
    ComplexString: function _complexStringConstructor() {
      this._fragments = [];
    },
    Idref: function _idrefConstructor(AST, parser) {
      // ASSERT(AST.type == "idref");
      // ASSERT(AST.children.length > 0);
      var i = 0;
      // ASSERT(AST.children[i].type == "ID");
      this.entity_ref = AST.children[i].value;
      this.index = [];
      var refslen = AST.children.length - 1;
      if (refslen > 0 && AST.children[refslen].type == "index") {
        this.index = parser.getIndex(AST.children[refslen]);
        --refslen;
      }
      this.refs = new Array(refslen);
      for (i = 1; i <= refslen; ++i) {
        // ASSERT(AST.children[i].type == "ID");
        this.refs[i - 1] = AST.children[i].value;
      }
    },
    MacroExpression: function _macroConstructor(AST, parser) {
      // ASSERT(AST.children.length > 0);
      this.idref = new l20n.service.expressions.Idref(AST.children[0]);
      this.params = new Array(AST.children.length - 1);
      var i;
      for (i = 1; i < AST.children.length; ++i) {
        this.params[i - 1] = parser.getExpression(AST.children[i]);
      }
      this.valueOf = function(context) {
        if (!context) {
          return "macro_call";
        }
        var macro = l20n.service.resolveEntity(this.idref.entity_ref, context);
        macro = macro.value;
        var i, pname, pval;
        var inner =  {
          resolveEntity: function _get(key){return this._m[key];},
          _m: {_locale: context.locale},
          parent: context
        };
        for (i = 0; i < macro.params.length; ++i) {
          pname = macro.params[i];
          if (i < this.params.length) {
            pval = this.params[i].valueOf(context);
          }
          else {
            pval = null;
          }
          inner._m[pname] = pval;
        }
        return macro.expression.valueOf(inner);
      };
    }
  }
};

l20n.service.LOL.prototype = {
  addEntity: function _addEntity(entity) {
    this._map[entity.ID] = entity;
  },
  get entities() {
    return this._map;
  }
};

l20n.service.Context.prototype = {
  getValue: function _getValue(idref) {
    var e = l20n.parser.parseString('${' + idref + '}s');
    return l20n.service.evalWith(e, this);
  },
  resolveEntity: function _resolveEntity(aKey) {
    var i;
    for (i = 0; i < this._lols.length; ++i) {
      if (aKey in this._lols[i].entities) {
        return this._lols[i].entities[aKey];
      }
    }
    return null;
  },
  set locale(code) {
    var i;
    if (code == this.locale) {
      // refresh?
      return;
    }
    this._locale = code;
    this._lols=[];
    for (i = 0; i < this._refs.length; ++i) {
      l20n.service.getLOL(this._refs[i], code, this);
    }
  },
  get locale() {
    return this._locale;
  },
  addReference: function _addReference(aRef) {
    this._refs.push(aRef);
    if (this.locale) {
      l20n.service.getLOL(aRef, this.locale, this);
    }
  },
  // Internal for use by service only
  addLOL: function _addLOL(lol) {
    this._lols.push(lol);
    // refresh?
  },
  get parent() {return this._parent;}
};

l20n.service.Entity.prototype = {
};

l20n.service.expressions.ComplexString.prototype = {
  push: function _push(fragment) {
    this._fragments.push(fragment);
  },
  get fragments() {return this._fragments;},
  valueOf: function _gv(context) {
    var i, val, parts = new Array(this._fragments.length);
    for (i = 0; i < this._fragments.length; ++i) {
      val = this._fragments[i].valueOf(context);
      if (val instanceof l20n.service.expressions.ComplexString) {
        val = val.valueOf(context);
      }
      parts[i] = val;
    }
    return parts.join('');
  }
};

l20n.service.expressions.Idref.prototype = {
  valueOf: function _gv(context) {
    var entity = null;
    var val = null;
    entity = l20n.service.resolveEntity(this.entity_ref, context);
    if (!(entity instanceof l20n.service.Entity)) {
      // shortcut for programmatic contexts
      return entity;
    }
    var obj, k = 0;
    // The second idref item could refer to a hash entry
    // XXX file bug to sort out what to do with conflicts
    // with attributes
    if (this.refs.length && typeof(entity.value) == "object" &&
        !(entity.value instanceof Array) &&
        (this.refs[k] in entity.value)) {
      obj = entity.value[this.refs[k]];
      val = obj;
      ++k;
    }
    else {
      // remaining idref items refer to attributes
      obj = entity.attributes;
    }
    while (k < this.refs.length && typeof(obj) == "object") {
      obj = obj[this.refs[k]];
      val = obj;
      ++k;
    }
    if (!val) {
      val = entity.value;
    }
    // might want to handle given index here, once we do that
    if ('index' in this && this.index && this.index.length) {
      val = this.handleIndex(this.index, val, context);
    }
    // end comment
    if ('index' in entity && entity.index && entity.index.length) {
      val = this.handleIndex(entity.index, val, context);
    }
    return val;
  },
  handleIndex: function(index, val, context) {
    k = 0;
    while (typeof(val) == "object" &&
           !(val instanceof l20n.service.expressions.ComplexString) &&
           k < index.length) {
      var indexval = index[k].valueOf(context);
      if (val instanceof Array) {
        // convert to number
        indexval = Number(indexval).valueOf();
      }
      if (!(indexval in val)) {
        LOG("cannot resolve " + entity.index[k]);
      }
      else {
        val = val[indexval];
      }
      ++k;
    }
    return val;
  }
};

function LOG(arg) {
  if (console && console.info) {console.info(arg);};
};


if (typeof dojo == 'object' && dojo.require) {
  /**
   * Dojo loader
   */
  dojo.require("dojo.io.*");
  l20n.service.loader = {
    start: function _start(url, callback) {
      var params = {
        url: url,
        load: callback,
        mimetype: "text/plain"
      };
      dojo.io.bind(params);
    }
  };
 }
 else {
   LOG("no loader found");
 }

