/**
 * @licstart The following is the entire license notice for the
 * Javascript code in this page
 *
 * Copyright 2021 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @licend The above is the entire license notice for the
 * Javascript code in this page
 */
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.Parser = exports.Errors = void 0;

var _formcalc_lexer = require("./formcalc_lexer.js");

const Errors = {
  assignment: "Invalid token in assignment.",
  block: "Invalid token in do ... end declaration.",
  elseif: "Invalid elseif declaration.",
  for: "Invalid token in for ... endfor declaration.",
  foreach: "Invalid token in foreach ... endfor declaration.",
  func: "Invalid token in func declaration.",
  if: "Invalid token if ... endif declaration.",
  index: "Invalid token in index.",
  params: "Invalid token in parameter list.",
  var: "Invalid token in var declaration.",
  while: "Invalid token while ... endwhile declaration."
};
exports.Errors = Errors;
const BUILTINS = new Set(["abs", "avg", "ceil", "count", "floor", "max", "min", "mod", "round", "sum", "date", "date2num", "datefmt", "isodate2num", "isotime2num", "localdatefmt", "localtimefmt", "num2date", "num2gmtime", "num2time", "time", "time2num", "timefmt", "apr", "cterm", "fv", "ipmt", "npv", "pmt", "ppmt", "pv", "rate", "term", "choose", "exists", "hasvalue", "oneof", "within", "at", "concat", "decode", "encode", "format", "left", "len", "lower", "ltrim", "parse", "replace", "right", "rtrim", "space", "str", "stuff", "substr", "uuid", "upper", "wordnum", "get", "post", "put", "eval", "ref", "unitvalue", "unittype", "acos", "asin", "atan", "cos", "deg2rad", "exp", "log", "pi", "pow", "rad2deg", "sin", "sqrt", "tan"]);
const LTR = true;
const RTL = false;
const Operators = {
  dot: {
    id: 0,
    prec: 0,
    assoc: RTL,
    nargs: 0,
    repr: "."
  },
  dotDot: {
    id: 1,
    prec: 0,
    assoc: RTL,
    nargs: 0,
    repr: ".."
  },
  dotHash: {
    id: 2,
    prec: 0,
    assoc: RTL,
    nargs: 0,
    repr: ".#"
  },
  call: {
    id: 1,
    prec: 1,
    assoc: LTR,
    nargs: 0
  },
  minus: {
    id: 4,
    nargs: 1,
    prec: 2,
    assoc: RTL,
    repr: "-",
    op: x => -x
  },
  plus: {
    id: 5,
    nargs: 1,
    prec: 2,
    assoc: RTL,
    repr: "+",
    op: x => +x
  },
  not: {
    id: 6,
    nargs: 1,
    prec: 2,
    assoc: RTL,
    repr: "!",
    op: x => !x ? 1 : 0
  },
  mul: {
    id: 7,
    nargs: 2,
    prec: 3,
    assoc: LTR,
    repr: "*",
    op: (x, y) => x * y
  },
  div: {
    id: 8,
    nargs: 2,
    prec: 3,
    assoc: LTR,
    repr: "/",
    op: (x, y) => x / y
  },
  add: {
    id: 9,
    nargs: 2,
    prec: 4,
    assoc: LTR,
    repr: "+",
    op: (x, y) => x + y
  },
  sub: {
    id: 10,
    nargs: 2,
    prec: 4,
    assoc: LTR,
    repr: "-",
    op: (x, y) => x - y
  },
  lt: {
    id: 11,
    nargs: 2,
    prec: 5,
    assoc: LTR,
    repr: "<",
    op: (x, y) => x < y ? 1 : 0
  },
  le: {
    id: 12,
    nargs: 2,
    prec: 5,
    assoc: LTR,
    repr: "<=",
    op: (x, y) => x <= y ? 1 : 0
  },
  gt: {
    id: 13,
    nargs: 2,
    prec: 5,
    assoc: LTR,
    repr: ">",
    op: (x, y) => x > y ? 1 : 0
  },
  ge: {
    id: 14,
    nargs: 2,
    prec: 5,
    assoc: LTR,
    repr: ">=",
    op: (x, y) => x >= y ? 1 : 0
  },
  eq: {
    id: 15,
    nargs: 2,
    prec: 6,
    assoc: LTR,
    repr: "===",
    op: (x, y) => x === y ? 1 : 0
  },
  ne: {
    id: 16,
    nargs: 2,
    prec: 6,
    assoc: LTR,
    repr: "!==",
    op: (x, y) => x !== y ? 1 : 0
  },
  and: {
    id: 17,
    nargs: 2,
    prec: 7,
    assoc: LTR,
    repr: "&&",
    op: (x, y) => x && y ? 1 : 0
  },
  or: {
    id: 18,
    nargs: 2,
    prec: 8,
    assoc: LTR,
    repr: "||",
    op: (x, y) => x || y ? 1 : 0
  },
  paren: {
    id: 19,
    prec: 9,
    assoc: RTL,
    nargs: 0
  },
  subscript: {
    id: 20,
    prec: 9,
    assoc: RTL,
    nargs: 0
  }
};
const OPERATOR = true;
const OPERAND = false;

class SimpleExprParser {
  constructor(lexer) {
    this.lexer = lexer;
    this.operands = [];
    this.operators = [];
    this.last = OPERATOR;
  }

  reset() {
    this.operands.length = 0;
    this.operators.length = 0;
    this.last = OPERATOR;
  }

  parse(tok) {
    tok = tok || this.lexer.next();

    while (true) {
      switch (tok.id) {
        case _formcalc_lexer.TOKEN.and:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.and);
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.divide:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.div);
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.dot:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.dot);
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.dotDot:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.dotDot);
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.dotHash:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.dotHash);
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.dotStar:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.dot);
            this.pushOperand(new AstEveryOccurence());
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.eq:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.eq);
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.ge:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.ge);
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.gt:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.gt);
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.le:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.le);
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.leftBracket:
          if (this.last === OPERAND) {
            this.flushWithOperator(Operators.subscript);
            const operand = this.operands.pop();
            const index = SimpleExprParser.parseIndex(this.lexer);
            this.operands.push(new AstSubscript(operand, index));
            this.last = OPERAND;
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.leftParen:
          if (this.last === OPERAND) {
            const lastOperand = this.operands[this.operands.length - 1];

            if (!(lastOperand instanceof AstIdentifier)) {
              return [tok, this.getNode()];
            }

            lastOperand.toLowerCase();
            const name = lastOperand.id;
            this.flushWithOperator(Operators.call);
            const callee = this.operands.pop();
            const params = SimpleExprParser.parseParams(this.lexer);

            if (callee instanceof AstIdentifier && BUILTINS.has(name)) {
              this.operands.push(new AstBuiltinCall(name, params));
            } else {
              this.operands.push(new AstCall(callee, params));
            }

            this.last = OPERAND;
          } else {
            this.operators.push(Operators.paren);
            this.last = OPERATOR;
          }

          break;

        case _formcalc_lexer.TOKEN.lt:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.lt);
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.minus:
          if (this.last === OPERATOR) {
            this.pushOperator(Operators.minus);
          } else {
            this.pushOperator(Operators.sub);
          }

          break;

        case _formcalc_lexer.TOKEN.ne:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.ne);
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.not:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.not);
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.null:
          if (this.last === OPERATOR) {
            this.pushOperand(new AstNull());
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.number:
          if (this.last === OPERATOR) {
            this.pushOperand(new AstNumber(tok.value));
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.or:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.or);
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.plus:
          if (this.last === OPERATOR) {
            this.pushOperator(Operators.plus);
          } else {
            this.pushOperator(Operators.add);
          }

          break;

        case _formcalc_lexer.TOKEN.rightBracket:
          if (!this.flushUntil(Operators.subscript.id)) {
            return [tok, this.getNode()];
          }

          break;

        case _formcalc_lexer.TOKEN.rightParen:
          if (!this.flushUntil(Operators.paren.id)) {
            return [tok, this.getNode()];
          }

          break;

        case _formcalc_lexer.TOKEN.string:
          if (this.last === OPERATOR) {
            this.pushOperand(new AstString(tok.value));
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.this:
          if (this.last === OPERATOR) {
            this.pushOperand(new AstThis());
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.times:
          if (this.last === OPERAND) {
            this.pushOperator(Operators.mul);
            break;
          }

          return [tok, this.getNode()];

        case _formcalc_lexer.TOKEN.identifier:
          if (this.last === OPERATOR) {
            this.pushOperand(new AstIdentifier(tok.value));
            break;
          }

          return [tok, this.getNode()];

        default:
          return [tok, this.getNode()];
      }

      tok = this.lexer.next();
    }
  }

  static parseParams(lexer) {
    const parser = new SimpleExprParser(lexer);
    const params = [];

    while (true) {
      const [tok, param] = parser.parse();

      if (param) {
        params.push(param);
      }

      if (tok.id === _formcalc_lexer.TOKEN.rightParen) {
        return params;
      } else if (tok.id !== _formcalc_lexer.TOKEN.comma) {
        throw new Error(Errors.params);
      }

      parser.reset();
    }
  }

  static parseIndex(lexer) {
    let tok = lexer.next();

    if (tok.id === _formcalc_lexer.TOKEN.times) {
      tok = lexer.next();

      if (tok.id !== _formcalc_lexer.TOKEN.rightBracket) {
        throw new Error(Errors.index);
      }

      return new AstEveryOccurence();
    }

    const [token, expr] = new SimpleExprParser(lexer).parse(tok);

    if (token.id !== _formcalc_lexer.TOKEN.rightBracket) {
      throw new Error(Errors.index);
    }

    return expr;
  }

  pushOperator(op) {
    this.flushWithOperator(op);
    this.operators.push(op);
    this.last = OPERATOR;
  }

  pushOperand(op) {
    this.operands.push(op);
    this.last = OPERAND;
  }

  operate(op) {
    if (op.nargs === 1) {
      const arg = this.operands.pop();
      this.operands.push(AstUnaryOperator.getOperatorOrValue(op, arg));
    } else {
      const arg2 = this.operands.pop();
      const arg1 = this.operands.pop();
      this.operands.push(AstBinaryOperator.getOperatorOrValue(op, arg1, arg2));
    }
  }

  flushWithOperator(op) {
    while (true) {
      const top = this.operators[this.operators.length - 1];

      if (top) {
        if (top.id >= 0 && SimpleExprParser.checkPrecedence(top, op)) {
          this.operators.pop();
          this.operate(top);
          continue;
        }
      }

      return;
    }
  }

  flush() {
    while (true) {
      const op = this.operators.pop();

      if (!op) {
        return;
      }

      this.operate(op);
    }
  }

  flushUntil(id) {
    while (true) {
      const op = this.operators.pop();

      if (!op) {
        return false;
      }

      if (op.id === id) {
        return true;
      }

      this.operate(op);
    }
  }

  getNode() {
    this.flush();
    return this.operands.pop();
  }

  static checkPrecedence(left, right) {
    return left.prec < right.prec || left.prec === right.prec && left.assoc === LTR;
  }

}

class Leaf {
  dump() {
    throw new Error("Not implemented method");
  }

  isSomPredicate() {
    return false;
  }

  isDotExpression() {
    return false;
  }

  isConstant() {
    return false;
  }

  toNumber() {
    return 0;
  }

  toComparable() {
    return null;
  }

}

class AstCall extends Leaf {
  constructor(callee, params) {
    super();
    this.callee = callee;
    this.params = params;
  }

  dump() {
    return {
      callee: this.callee.dump(),
      params: this.params.map(x => x.dump())
    };
  }

}

class AstBuiltinCall extends Leaf {
  constructor(id, params) {
    super();
    this.id = id;
    this.params = params;
  }

  dump() {
    return {
      builtin: this.id,
      params: this.params.map(x => x.dump())
    };
  }

}

class AstSubscript extends Leaf {
  constructor(operand, index) {
    super();
    this.operand = operand;
    this.index = index;
  }

  dump() {
    return {
      operand: this.operand.dump(),
      index: this.index.dump()
    };
  }

}

class AstBinaryOperator extends Leaf {
  constructor(id, left, right, repr) {
    super();
    this.id = id;
    this.left = left;
    this.right = right;
    this.repr = repr;
  }

  dump() {
    return {
      operator: this.repr,
      left: this.left.dump(),
      right: this.right.dump()
    };
  }

  isDotExpression() {
    return Operators.id.dot <= this.id && this.id <= Operators.id.dotHash;
  }

  isSomPredicate() {
    return this.isDotExpression() || Operators.id.lt <= this.id && this.id <= Operators.id.or && (this.left.isDotExpression() && this.right.isConstant() || this.left.isConstant() && this.right.isDotExpression() || this.left.isDotExpression() && this.right.isDotExpression());
  }

  static getOperatorOrValue(operator, left, right) {
    if (!left.isConstant() || !right.isConstant()) {
      return new AstBinaryOperator(operator.id, left, right, operator.repr);
    }

    if (Operators.lt.id <= operator.id && operator.id <= Operators.ne.id && !(left instanceof AstNumber) && !(right instanceof AstNumber)) {
      return new AstNumber(operator.op(left.toComparable(), right.toComparable()));
    }

    return new AstNumber(operator.op(left.toNumber(), right.toNumber()));
  }

}

class AstUnaryOperator extends Leaf {
  constructor(id, arg, repr) {
    super();
    this.id = id;
    this.arg = arg;
    this.repr = repr;
  }

  dump() {
    return {
      operator: this.repr,
      arg: this.arg.dump()
    };
  }

  static getOperatorOrValue(operator, arg) {
    if (!arg.isConstant()) {
      return new AstUnaryOperator(operator.id, arg, operator.repr);
    }

    return new AstNumber(operator.op(arg.toNumber()));
  }

}

class AstNumber extends Leaf {
  constructor(number) {
    super();
    this.number = number;
  }

  dump() {
    return this.number;
  }

  isConstant() {
    return true;
  }

  toNumber() {
    return this.number;
  }

}

class AstString extends Leaf {
  constructor(str) {
    super();
    this.str = str;
  }

  dump() {
    return this.str;
  }

  isConstant() {
    return true;
  }

  toNumber() {
    return !isNaN(this.str) ? parseFloat(this.str) : 0;
  }

  toComparable() {
    return this.str;
  }

}

class AstThis extends Leaf {
  dump() {
    return {
      special: "this"
    };
  }

}

class AstIdentifier extends Leaf {
  constructor(id) {
    super();
    this.id = id;
  }

  dump() {
    return {
      id: this.id
    };
  }

  toLowerCase() {
    this.id = this.id.toLowerCase();
  }

}

class AstNull extends Leaf {
  dump() {
    return {
      special: null
    };
  }

  isConstant() {
    return true;
  }

  toComparable() {
    return null;
  }

}

class AstEveryOccurence {
  dump() {
    return {
      special: "*"
    };
  }

}

class VarDecl extends Leaf {
  constructor(id, expr) {
    super();
    this.id = id;
    this.expr = expr;
  }

  dump() {
    return {
      var: this.id,
      expr: this.expr.dump()
    };
  }

}

class Assignment extends Leaf {
  constructor(id, expr) {
    super();
    this.id = id;
    this.expr = expr;
  }

  dump() {
    return {
      assignment: this.id,
      expr: this.expr.dump()
    };
  }

}

class FuncDecl extends Leaf {
  constructor(id, params, body) {
    super();
    this.id = id;
    this.params = params;
    this.body = body;
  }

  dump() {
    return {
      func: this.id,
      params: this.params,
      body: this.body.dump()
    };
  }

}

class IfDecl extends Leaf {
  constructor(condition, thenClause, elseIfClause, elseClause) {
    super();
    this.condition = condition;
    this.then = thenClause;
    this.elseif = elseIfClause;
    this.else = elseClause;
  }

  dump() {
    return {
      decl: "if",
      condition: this.condition.dump(),
      then: this.then.dump(),
      elseif: this.elseif ? this.elseif.map(x => x.dump()) : null,
      else: this.else ? this.else.dump() : null
    };
  }

}

class ElseIfDecl extends Leaf {
  constructor(condition, thenClause) {
    super();
    this.condition = condition;
    this.then = thenClause;
  }

  dump() {
    return {
      decl: "elseif",
      condition: this.condition.dump(),
      then: this.then.dump()
    };
  }

}

class WhileDecl extends Leaf {
  constructor(condition, whileClause) {
    super();
    this.condition = condition;
    this.body = whileClause;
  }

  dump() {
    return {
      decl: "while",
      condition: this.condition.dump(),
      body: this.body.dump()
    };
  }

}

class ForDecl extends Leaf {
  constructor(assignment, upto, end, step, body) {
    super();
    this.assignment = assignment;
    this.upto = upto;
    this.end = end;
    this.step = step;
    this.body = body;
  }

  dump() {
    return {
      decl: "for",
      assignment: this.assignment.dump(),
      type: this.upto ? "upto" : "downto",
      end: this.end.dump(),
      step: this.step ? this.step.dump() : null,
      body: this.body.dump()
    };
  }

}

class ForeachDecl extends Leaf {
  constructor(id, params, body) {
    super();
    this.id = id;
    this.params = params;
    this.body = body;
  }

  dump() {
    return {
      decl: "foreach",
      id: this.id,
      params: this.params.map(x => x.dump()),
      body: this.body.dump()
    };
  }

}

class BlockDecl extends Leaf {
  constructor(body) {
    super();
    this.body = body;
  }

  dump() {
    return {
      decl: "block",
      body: this.body.dump()
    };
  }

}

class ExprList extends Leaf {
  constructor(expressions) {
    super();
    this.expressions = expressions;
  }

  dump() {
    return this.expressions.map(x => x.dump());
  }

}

class BreakDecl extends Leaf {
  dump() {
    return {
      special: "break"
    };
  }

}

class ContinueDecl extends Leaf {
  dump() {
    return {
      special: "continue"
    };
  }

}

class Parser {
  constructor(code) {
    this.lexer = new _formcalc_lexer.Lexer(code);
  }

  parse() {
    const [tok, decls] = this.parseExprList();

    if (tok.id !== _formcalc_lexer.TOKEN.eof) {
      throw new Error("Invalid token in Form code");
    }

    return decls;
  }

  parseExprList() {
    const expressions = [];
    let tok = null,
        expr;

    while (true) {
      [tok, expr] = this.parseExpr(tok);

      if (!expr) {
        return [tok, new ExprList(expressions)];
      }

      expressions.push(expr);
    }
  }

  parseExpr(tok) {
    tok = tok || this.lexer.next();

    switch (tok.id) {
      case _formcalc_lexer.TOKEN.identifier:
        return this.parseAssigmentOrExpr(tok);

      case _formcalc_lexer.TOKEN.break:
        return [null, new BreakDecl()];

      case _formcalc_lexer.TOKEN.continue:
        return [null, new ContinueDecl()];

      case _formcalc_lexer.TOKEN.do:
        return this.parseBlock();

      case _formcalc_lexer.TOKEN.for:
        return this.parseFor();

      case _formcalc_lexer.TOKEN.foreach:
        return this.parseForeach();

      case _formcalc_lexer.TOKEN.func:
        return this.parseFuncDecl();

      case _formcalc_lexer.TOKEN.if:
        return this.parseIf();

      case _formcalc_lexer.TOKEN.var:
        return this.parseVarDecl();

      case _formcalc_lexer.TOKEN.while:
        return this.parseWhile();

      default:
        return this.parseSimpleExpr(tok);
    }
  }

  parseAssigmentOrExpr(tok) {
    const savedTok = tok;
    tok = this.lexer.next();

    if (tok.id === _formcalc_lexer.TOKEN.assign) {
      const [tok1, expr] = this.parseSimpleExpr(null);
      return [tok1, new Assignment(savedTok.value, expr)];
    }

    const parser = new SimpleExprParser(this.lexer);
    parser.pushOperand(new AstIdentifier(savedTok.value));
    return parser.parse(tok);
  }

  parseBlock() {
    const [tok1, body] = this.parseExprList();
    const tok = tok1 || this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.end) {
      throw new Error(Errors.block);
    }

    return [null, new BlockDecl(body)];
  }

  parseVarDecl() {
    let tok = this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.identifier) {
      throw new Error(Errors.var);
    }

    const identifier = tok.value;
    tok = this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.assign) {
      return [tok, new VarDecl(identifier, null)];
    }

    const [tok1, expr] = this.parseSimpleExpr();
    return [tok1, new VarDecl(identifier, expr)];
  }

  parseFuncDecl() {
    let tok = this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.identifier) {
      throw new Error(Errors.func);
    }

    const identifier = tok.value;
    const params = this.parseParamList();
    tok = this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.do) {
      throw new Error(Errors.func);
    }

    const [tok1, body] = this.parseExprList();
    tok = tok1 || this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.endfunc) {
      throw new Error(Errors.func);
    }

    return [null, new FuncDecl(identifier, params, body)];
  }

  parseParamList() {
    const params = [];
    let tok = this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.leftParen) {
      throw new Error(Errors.func);
    }

    tok = this.lexer.next();

    if (tok.id === _formcalc_lexer.TOKEN.rightParen) {
      return params;
    }

    while (true) {
      if (tok.id !== _formcalc_lexer.TOKEN.identifier) {
        throw new Error(Errors.func);
      }

      params.push(tok.value);
      tok = this.lexer.next();

      if (tok.id === _formcalc_lexer.TOKEN.rightParen) {
        return params;
      }

      if (tok.id !== _formcalc_lexer.TOKEN.comma) {
        throw new Error(Errors.func);
      }

      tok = this.lexer.next();
    }
  }

  parseSimpleExpr(tok = null) {
    return new SimpleExprParser(this.lexer).parse(tok);
  }

  parseIf() {
    let elseIfClause = [];
    let tok = this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.leftParen) {
      throw new Error(Errors.if);
    }

    const [tok1, condition] = this.parseSimpleExpr();
    tok = tok1 || this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.rightParen) {
      throw new Error(Errors.if);
    }

    tok = this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.then) {
      throw new Error(Errors.if);
    }

    const [tok2, thenClause] = this.parseExprList();
    tok = tok2 || this.lexer.next();

    while (tok.id === _formcalc_lexer.TOKEN.elseif) {
      tok = this.lexer.next();

      if (tok.id !== _formcalc_lexer.TOKEN.leftParen) {
        throw new Error(Errors.elseif);
      }

      const [tok3, elseIfCondition] = this.parseSimpleExpr();
      tok = tok3 || this.lexer.next();

      if (tok.id !== _formcalc_lexer.TOKEN.rightParen) {
        throw new Error(Errors.elseif);
      }

      tok = this.lexer.next();

      if (tok.id !== _formcalc_lexer.TOKEN.then) {
        throw new Error(Errors.elseif);
      }

      const [tok4, elseIfThenClause] = this.parseExprList();
      elseIfClause.push(new ElseIfDecl(elseIfCondition, elseIfThenClause));
      tok = tok4 || this.lexer.next();
    }

    if (elseIfClause.length === 0) {
      elseIfClause = null;
    }

    if (tok.id === _formcalc_lexer.TOKEN.endif) {
      return [null, new IfDecl(condition, thenClause, elseIfClause, null)];
    }

    if (tok.id !== _formcalc_lexer.TOKEN.else) {
      throw new Error(Errors.if);
    }

    const [tok5, elseClause] = this.parseExprList();
    tok = tok5 || this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.endif) {
      throw new Error(Errors.if);
    }

    return [null, new IfDecl(condition, thenClause, elseIfClause, elseClause)];
  }

  parseWhile() {
    let tok = this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.leftParen) {
      throw new Error(Errors.while);
    }

    const [tok1, condition] = this.parseSimpleExpr();
    tok = tok1 || this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.rightParen) {
      throw new Error(Errors.while);
    }

    tok = this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.do) {
      throw new Error(Errors.while);
    }

    const [tok2, whileClause] = this.parseExprList();
    tok = tok2 || this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.endwhile) {
      throw new Error(Errors.while);
    }

    return [null, new WhileDecl(condition, whileClause)];
  }

  parseAssignment() {
    let tok = this.lexer.next();
    let hasVar = false;

    if (tok.id === _formcalc_lexer.TOKEN.var) {
      hasVar = true;
      tok = this.lexer.next();
    }

    if (tok.id !== _formcalc_lexer.TOKEN.identifier) {
      throw new Error(Errors.assignment);
    }

    const identifier = tok.value;
    tok = this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.assign) {
      throw new Error(Errors.assignment);
    }

    const [tok1, expr] = this.parseSimpleExpr();

    if (hasVar) {
      return [tok1, new VarDecl(identifier, expr)];
    }

    return [tok1, new Assignment(identifier, expr)];
  }

  parseFor() {
    let tok,
        step = null;
    let upto = false;
    const [tok1, assignment] = this.parseAssignment();
    tok = tok1 || this.lexer.next();

    if (tok.id === _formcalc_lexer.TOKEN.upto) {
      upto = true;
    } else if (tok.id !== _formcalc_lexer.TOKEN.downto) {
      throw new Error(Errors.for);
    }

    const [tok2, end] = this.parseSimpleExpr();
    tok = tok2 || this.lexer.next();

    if (tok.id === _formcalc_lexer.TOKEN.step) {
      [tok, step] = this.parseSimpleExpr();
      tok = tok || this.lexer.next();
    }

    if (tok.id !== _formcalc_lexer.TOKEN.do) {
      throw new Error(Errors.for);
    }

    const [tok3, body] = this.parseExprList();
    tok = tok3 || this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.endfor) {
      throw new Error(Errors.for);
    }

    return [null, new ForDecl(assignment, upto, end, step, body)];
  }

  parseForeach() {
    let tok = this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.identifier) {
      throw new Error(Errors.foreach);
    }

    const identifier = tok.value;
    tok = this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.in) {
      throw new Error(Errors.foreach);
    }

    tok = this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.leftParen) {
      throw new Error(Errors.foreach);
    }

    const params = SimpleExprParser.parseParams(this.lexer);
    tok = this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.do) {
      throw new Error(Errors.foreach);
    }

    const [tok1, body] = this.parseExprList();
    tok = tok1 || this.lexer.next();

    if (tok.id !== _formcalc_lexer.TOKEN.endfor) {
      throw new Error(Errors.foreach);
    }

    return [null, new ForeachDecl(identifier, params, body)];
  }

}

exports.Parser = Parser;