text: import * as utils from '../utility';
  import {isFilter} from '../view';
  import {NullObject} from './null-object';
  
  var AST = {};
  AST.Program = 'Program';
  AST.ExpressionStatement = 'ExpressionStatement';
  AST.AssignmentExpression = 'AssignmentExpression';
  AST.ConditionalExpression = 'ConditionalExpression';
  AST.LogicalExpression = 'LogicalExpression';
  AST.BinaryExpression = 'BinaryExpression';
  AST.UnaryExpression = 'UnaryExpression';
  AST.CallExpression = 'CallExpression';
  AST.MemberExpression = 'MemberExpression';
  AST.Identifier = 'Identifier';
  AST.Literal = 'Literal';
  AST.ArrayExpression = 'ArrayExpression';
  AST.Property = 'Property';
  AST.ObjectExpression = 'ObjectExpression';
  
  function AstNode(type) {
      this.type = type;
      this.childNodes = [];
  }
  
  AstNode.prototype.appendChild = function (child) {
      this.childNodes.push(child);
  };
  
  AstNode.prototype.compute = function (scope, options, context) {
      return this.childNodes.map(function (child) {
          return child.compute(scope, options || {}, context);
      });
  };
  
  // has condition or call expression
  AstNode.prototype.hasCC = function () {
      return this.childNodes.some(function (child) {
          return child.hasCC();
      });
  };
  
  utils.inherit(ProgramNode, AstNode);
  function ProgramNode() {
      ProgramNode.super.call(this, AST.Program);
  }
  
  ProgramNode.prototype.compute = function (scope, options) {
      var result = this.childNodes[0].compute(scope, options || {});
  
      if (result instanceof NullObject) {
          return null;
      }
  
      return result;
  };
  
  ProgramNode.prototype.getMemberExpression = function () {
      return this.childNodes[0].getMemberExpression();
  };
  
  utils.inherit(ExpressionStatementNode, AstNode);
  function ExpressionStatementNode() {
      ExpressionStatementNode.super.call(this, AST.ExpressionStatement);
  }
  
  ExpressionStatementNode.prototype.compute = function (scope, options, context) {
      return this.childNodes[0].compute(scope, options || {}, context);
  };
  
  ExpressionStatementNode.prototype.getMemberExpression = function () {
      var child = this.childNodes[0];
  
      if(child instanceof MemberExpressionNode){
          return child;
      }
  
      return null;
  };
  
  utils.inherit(AssignmentExpressionNode, AstNode);
  function AssignmentExpressionNode(left, right, operator) {
      AssignmentExpressionNode.super.call(this, AST.AssignmentExpression);
      this.left = left;
      this.right = right;
      this.operator = operator;
  }
  
  AssignmentExpressionNode.prototype.compute = function (scope, options) {
      var target = this.left.compute(scope, options, { assignmentLeft: true });
      var value = this.right.compute(scope, options);
      target.obj[target.prop] = value;
  };
  
  utils.inherit(ConditionalExpressionNode, AstNode);
  function ConditionalExpressionNode(test, alternate, consequent) {
      ConditionalExpressionNode.super.call(this, AST.ConditionalExpression);
      this.test = test;
      this.alternate = alternate;
      this.consequent = consequent;
  }
  
  ConditionalExpressionNode.prototype.compute = function (scope, options) {
      if (this.test.compute(scope, options)) {
          return this.alternate.compute(scope, options);
      }
      return this.consequent.compute(scope, options);
  };
  
  // has condition or call expression
  ConditionalExpressionNode.prototype.hasCC = function () {
      return true;
  };
  
  utils.inherit(LogicalExpressionNode, AstNode);
  function LogicalExpressionNode(operator, left, right) {
      LogicalExpressionNode.super.call(this, AST.LogicalExpression);
      this.operator = operator;
      this.left = left;
      this.right = right;
  }
  
  LogicalExpressionNode.prototype.compute = function (scope, options) {
      var result;
      var leftValue = this.left.compute(scope, options);
      var rightValue = this.right.compute(scope, options);
  
      switch (this.operator) {
          case '&&':
              result = leftValue && rightValue;
              break;
          case '||':
              result = leftValue && rightValue;
              break;
      }
  
      return result;
  };
  
  utils.inherit(BinaryExpressionNode, AstNode);
  function BinaryExpressionNode(operator, left, right) {
      BinaryExpressionNode.super.call(this, AST.BinaryExpression);
      this.operator = operator;
      this.left = left;
      this.right = right;
  }
  
  BinaryExpressionNode.prototype.compute = function (scope, options) {
      var result;
      var leftValue = this.left.compute(scope, options);
      var rightValue = this.right.compute(scope, options);
  
      // '+', '-', '*', '/', '%', '<', '>', '<=', '>=', '==', '!=', '===', '!=='
      switch (this.operator) {
          case '+':
              result = leftValue + rightValue;
              break;
          case '-':
              result = leftValue - rightValue;
              break;
          case '*':
              result = leftValue * rightValue;
              break;
          case '/':
              result = leftValue / rightValue;
              break;
          case '%':
              result = leftValue % rightValue;
              break;
          case '<':
              result = leftValue < rightValue;
              break;
          case '>':
              result = leftValue > rightValue;
              break;
          case '<=':
              result = leftValue <= rightValue;
              break;
          case '>=':
              result = leftValue >= rightValue;
              break;
          case '==':
              result = leftValue == rightValue;
              break;
          case '!=':
              result = leftValue != rightValue;
              break;
          case '===':
              result = leftValue === rightValue;
              break;
          case '!==':
              result = leftValue !== rightValue;
              break;
      }
  
      return result;
  };
  
  utils.inherit(UnaryExpressionNode, AstNode);
  function UnaryExpressionNode(operator, arg) {
      UnaryExpressionNode.super.call(this, AST.UnaryExpression);
      this.prefix = true;
      this.operator = operator;
      this.arg = arg;
  }
  
  UnaryExpressionNode.prototype.compute = function (scope, options) {
      var result, value = this.arg.compute(scope, options);
  
      switch (this.operator) {
          case '+':
              result = value;
              break;
          case '-':
              result = -value;
              break;
          case '!':
              result = !value;
              break;
      }
  
      return result;
  };
  
  utils.inherit(LiteralNode, AstNode);
  function LiteralNode(value) {
      LiteralNode.super.call(this, AST.Literal);
      this.value = value;
  }
  
  LiteralNode.prototype.compute = function () {
      return this.value;
  };
  
  LiteralNode.prototype.toText = function () {
      return this.value;
  };
  
  utils.inherit(CallExpressionNode, AstNode);
  function CallExpressionNode(callee, args) {
      CallExpressionNode.super.call(this, AST.CallExpression);
      this.callee = callee;
      this.args = args;
      this.filter = false;
  }
  
  CallExpressionNode.prototype.compute = function (scope, options) {
      var context = this.callee.compute(scope, options, {callee: true});
      var argValues = this.args.map(function (arg) {
          return arg.compute(scope, options);
      });
  
      if (this.filter) {
          var filterIns = options.createFilter(context.prop);
          if (isFilter(filterIns)) {
              return filterIns.$execute.apply(filterIns, argValues);
          }
          throw new Error('Filter "' + context.prop + '" is not defined');
      }
      else {
          var fn = context.obj[context.prop];
          if (utils.isFunction(fn)) {
              return fn.apply(context.obj, argValues);
          }
          throw new Error(context.prop + ' is not a function');
      }
  };
  
  // has condition or call expression
  CallExpressionNode.prototype.hasCC = function () {
      return !this.filter;
  };
  
  utils.inherit(MemberExpressionNode, AstNode);
  function MemberExpressionNode(object, property, computed) {
      MemberExpressionNode.super.call(this, AST.MemberExpression);
      this.object = object;
      this.property = property;
      this.computed = computed;
      this.allowNull = false;
  }
  
  MemberExpressionNode.prototype.compute = function (scope, options, context) {
      var obj = this.object.compute(scope, options);
  
      if (obj == null && this.object.allowNull) {
          obj = new NullObject();
      }
  
      if (this.computed) {
          var prop = this.property.compute(scope, options, context);
          if (utils.isArray(options.members)) {
              options.members.push({
                  target: obj,
                  targetKey: this.object.toText(),
                  key: prop
              });
          }
          return obj[prop];
      }
  
      return this.property.compute(obj, options, context, this.object);
  };
  
  MemberExpressionNode.prototype.toText = function () {
      return this.object.toText() + '.' + this.property.toText();
  };
  
  utils.inherit(IdentifierNode, AstNode);
  function IdentifierNode(name) {
      IdentifierNode.super.call(this, AST.Identifier);
      this.name = name;
      this.allowNull = false;
  }
  
  IdentifierNode.prototype.compute = function (obj, options, context, objNode) {
      if (context) {
          if (context.assignmentLeft || context.callee) {
              return {
                  obj: obj,
                  prop: this.name
              };
          }
          else if (context.propertyKey) {
              return this.name;
          }
      }
  
      var result = obj[this.name];
  
      if (utils.isArray(options.members)) {
          options.members.push({
              target: obj,
              targetKey: objNode != null ? objNode.toText() : null,
              key: this.name
          });
      }
  
      if (result == null && options.locals) {
          result = options.locals[this.name];
      }
  
      return result;
  };
  
  IdentifierNode.prototype.toText = function () {
      return this.name;
  };
  
  utils.inherit(ArrayExpressionNode, AstNode);
  function ArrayExpressionNode() {
      ArrayExpressionNode.super.call(this, AST.ArrayExpression);
  }
  
  utils.inherit(PropertyNode, AstNode);
  function PropertyNode() {
      PropertyNode.super.call(this, AST.Property);
      this.kind = 'init';
      this.key = null;
      this.value = null;
      this.computed = false;
  }
  
  PropertyNode.prototype.compute = function (scope, options) {
      return {
          key: this.key.compute(scope, options, { propertyKey: true }),
          value: this.value.compute(scope, options)
      };
  };
  
  utils.inherit(ObjectExpressionNode, AstNode);
  function ObjectExpressionNode(properties) {
      ObjectExpressionNode.super.call(this, AST.ObjectExpression);
      this.properties = properties;
  }
  
  ObjectExpressionNode.prototype.compute = function (scope, options) {
      var result = {};
  
      this.properties.forEach(function (item) {
          var def = item.compute(scope, options);
          result[def.key] = def.value;
      });
  
      return result;
  };
  
  function isAssignable(ast) {
      return ast.type === AST.Identifier || ast.type === AST.MemberExpression;
  }
  
  function Parser(lexer, options) {
      this.lexer = lexer;
      this.options = options;
  }
  
  Parser.prototype.throwError = function (msg, token) {
      throw new Error('Syntax Error: Token \'' + token.text + '\'' + msg + ' at column ' + (token.index + 1) +
          ' of the expression [' + this.text + '] starting at [' + this.text.substring(token.index) + '].');
  };
  
  Parser.prototype.parse = function (text) {
      this.text = text;
      this.tokens = this.lexer.lex(text);
  
      var value = this.program();
  
      if (this.tokens.length !== 0) {
          this.throwError('is an unexpected token', this.tokens[0]);
      }
  
      return value;
  };
  
  Parser.prototype.program = function () {
      var program = new ProgramNode();
      while (true) {
          if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) {
              program.appendChild(this.expressionStatement());
          }
          if (!this.expect(';')) {
              return program;
          }
      }
  };
  
  Parser.prototype.expressionStatement = function () {
      var exp = new ExpressionStatementNode();
      exp.appendChild(this.filterChain());
      return exp;
  };
  
  Parser.prototype.filterChain = function () {
      var left = this.expression();
      while (this.expect('|')) {
          left = this.filter(left);
      }
      return left;
  };
  
  Parser.prototype.expression = function () {
      return this.assignment();
  };
  
  Parser.prototype.assignment = function () {
      var result = this.ternary();
      if (this.expect('=')) {
          if (!isAssignable(result)) {
              throw new Error('Trying to assign a value to a non l-value');
          }
  
          result = new AssignmentExpressionNode(result, this.assignment(), '=');
      }
      return result;
  };
  
  Parser.prototype.ternary = function () {
      var test = this.logicalOR();
      var alternate;
      var consequent;
      if (this.expect('?')) {
          alternate = this.expression();
          if (this.consume(':')) {
              consequent = this.expression();
              return new ConditionalExpressionNode(test, alternate, consequent);
          }
      }
      return test;
  };
  
  Parser.prototype.logicalOR = function () {
      var left = this.logicalAND();
      while (this.expect('||')) {
          left = new LogicalExpressionNode('||', left, this.logicalAND());
      }
      return left;
  };
  
  Parser.prototype.logicalAND = function () {
      var left = this.equality();
      while (this.expect('&&')) {
          left = new LogicalExpressionNode('&&', left, this.equality());
      }
      return left;
  };
  
  Parser.prototype.equality = function () {
      var left = this.relational();
      var token;
      while ((token = this.expect('==', '!=', '===', '!=='))) {
          left = new BinaryExpressionNode(token.text, left, this.relational());
      }
      return left;
  };
  
  Parser.prototype.relational = function () {
      var left = this.additive();
      var token;
      while ((token = this.expect('<', '>', '<=', '>='))) {
          left = new BinaryExpressionNode(token.text, left, this.additive());
      }
      return left;
  };
  
  Parser.prototype.additive = function () {
      var left = this.multiplicative();
      var token;
      while ((token = this.expect('+', '-'))) {
          left = new BinaryExpressionNode(token.text, left, this.multiplicative());
      }
      return left;
  };
  
  Parser.prototype.multiplicative = function () {
      var left = this.unary();
      var token;
      while ((token = this.expect('*', '/', '%'))) {
          left = new BinaryExpressionNode(token.text, left, this.unary());
      }
      return left;
  };
  
  Parser.prototype.unary = function () {
      var token;
      if ((token = this.expect('+', '-', '!'))) {
          return new UnaryExpressionNode(token.text, this.unary());
      } else {
          return this.primary();
      }
  };
  
  Parser.prototype.primary = function () {
      var primary;
      if (this.expect('(')) {
          primary = this.filterChain();
          this.consume(')');
      } else if (this.expect('[')) {
          primary = this.arrayDeclaration();
      } else if (this.expect('{')) {
          primary = this.object();
      } else if (this.options.literals.hasOwnProperty(this.peek().text)) {
          primary = new LiteralNode(this.options.literals[this.consume().text]);
      } else if (this.peek().identifier) {
          primary = this.identifier();
      } else if (this.peek().constant) {
          primary = this.constant();
      } else {
          this.throwError('not a primary expression', this.peek());
      }
  
      var next;
      while (this.peek('(', '[', '.') || (this.peek('?') && this.peekAhead(1, '.'))) {
          if (this.expect('?')) {
              primary.allowNull = true;
          }
  
          next = this.expect('(', '[', '.');
  
          if (next.text === '(') {
              primary = new CallExpressionNode(primary, this.parseArguments());
              this.consume(')');
          } else if (next.text === '[') {
              primary = new MemberExpressionNode(primary, this.expression(), true);
              this.consume(']');
          } else if (next.text === '.') {
              primary = new MemberExpressionNode(primary, this.identifier(), false);
          } else {
              this.throwError('IMPOSSIBLE');
          }
      }
      return primary;
  };
  
  Parser.prototype.filter = function (baseExpression) {
      var args = [baseExpression], identifiers = [this.identifier()];
  
      while (this.expect('.')) {
          identifiers.push(this.identifier());
      }
  
      // plus namespace
      var entireName = identifiers.map(function (item) {
          return item.name;
      }).join('.');
  
      var result = new CallExpressionNode(new IdentifierNode(entireName), args);
  
      result.filter = true;
  
      while (this.expect(':')) {
          args.push(this.expression());
      }
  
      return result;
  };
  
  Parser.prototype.parseArguments = function () {
      var args = [];
      if (this.peekToken().text !== ')') {
          do {
              args.push(this.filterChain());
          } while (this.expect(','));
      }
      return args;
  };
  
  Parser.prototype.identifier = function () {
      var token = this.consume();
      if (!token.identifier) {
          this.throwError('is not a valid identifier', token);
      }
      return new IdentifierNode(token.text);
  };
  
  Parser.prototype.constant = function () {
      // TODO check that it is a constant
      return new LiteralNode(this.consume().value);
  };
  
  Parser.prototype.arrayDeclaration = function () {
      var arr = new ArrayExpressionNode();
      if (this.peekToken().text !== ']') {
          do {
              if (this.peek(']')) {
                  // Support trailing commas per ES5.1.
                  break;
              }
              arr.appendChild(this.expression());
          } while (this.expect(','));
      }
      this.consume(']');
  
      return arr;
  };
  
  Parser.prototype.object = function () {
      var properties = [], property;
      if (this.peekToken().text !== '}') {
          do {
              if (this.peek('}')) {
                  // Support trailing commas per ES5.1.
                  break;
              }
              property = new PropertyNode();
              if (this.peek().constant) {
                  property.key = this.constant();
                  property.computed = false;
                  this.consume(':');
                  property.value = this.expression();
              } else if (this.peek().identifier) {
                  property.key = this.identifier();
                  property.computed = false;
                  if (this.peek(':')) {
                      this.consume(':');
                      property.value = this.expression();
                  } else {
                      property.value = property.key;
                  }
              } else if (this.peek('[')) {
                  this.consume('[');
                  property.key = this.expression();
                  this.consume(']');
                  property.computed = true;
                  this.consume(':');
                  property.value = this.expression();
              } else {
                  this.throwError('invalid key', this.peek());
              }
              properties.push(property);
          } while (this.expect(','));
      }
      this.consume('}');
  
      return new ObjectExpressionNode(properties);
  };
  
  Parser.prototype.consume = function (e1) {
      if (this.tokens.length === 0) {
          throw new Error('Unexpected end of expression: ' + this.text);
      }
  
      var token = this.expect(e1);
      if (!token) {
          this.throwError('is unexpected, expecting [' + e1 + ']', this.peek());
      }
      return token;
  };
  
  Parser.prototype.peekToken = function () {
      if (this.tokens.length === 0) {
          throw new Error('Unexpected end of expression: ' + this.text);
      }
      return this.tokens[0];
  };
  
  Parser.prototype.peek = function (e1, e2, e3, e4) {
      return this.peekAhead(0, e1, e2, e3, e4);
  };
  
  Parser.prototype.peekAhead = function (i, e1, e2, e3, e4) {
      if (this.tokens.length > i) {
          var token = this.tokens[i];
          var t = token.text;
          if (t === e1 || t === e2 || t === e3 || t === e4 ||
              (!e1 && !e2 && !e3 && !e4)) {
              return token;
          }
      }
      return false;
  };
  
  Parser.prototype.expect = function (e1, e2, e3, e4) {
      var token = this.peek(e1, e2, e3, e4);
      if (token) {
          this.tokens.shift();
          return token;
      }
      return false;
  };
  
  export { Parser };