/*
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Rhino code, released
 * May 6, 1999.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1997-1999
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   John Lenz
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL"), in which
 * case the provisions of the GPL are applicable instead of those above. If
 * you wish to allow use of your version of this file only under the terms of
 * the GPL and not to allow others to use your version of this file under the
 * MPL, indicate your decision by deleting the provisions above and replacing
 * them with the notice and other provisions required by the GPL. If you do
 * not delete the provisions above, a recipient may use your version of this
 * file under either the MPL or the GPL.
 *
 * ***** END LICENSE BLOCK ***** */

package com.google.javascript.rhino;

import com.google.common.base.Preconditions;

import java.util.List;

/**
 * An AST construction helper class
 * @author johnlenz@google.com (John Lenz)
 */
public class IR {

  private IR() {}

  public static Node empty() {
    return new Node(Token.EMPTY);
  }

  public static Node function(Node name, Node params, Node body) {
    Preconditions.checkState(name.isName());
    Preconditions.checkState(params.isParamList());
    Preconditions.checkState(body.isBlock());
    return new Node(Token.FUNCTION, name, params, body);
  }

  public static Node paramList() {
    return new Node(Token.PARAM_LIST);
  }

  public static Node paramList(Node param) {
    Preconditions.checkState(param.isName() || param.isRest());
    return new Node(Token.PARAM_LIST, param);
  }

  public static Node paramList(Node ... params) {
    Node paramList = paramList();
    for (Node param : params) {
      Preconditions.checkState(param.isName() || param.isRest());
      paramList.addChildToBack(param);
    }
    return paramList;
  }

  public static Node paramList(List<Node> params) {
    Node paramList = paramList();
    for (Node param : params) {
      Preconditions.checkState(param.isName() || param.isRest());
      paramList.addChildToBack(param);
    }
    return paramList;
  }

  public static Node block() {
    Node block = new Node(Token.BLOCK);
    return block;
  }

  public static Node block(Node stmt) {
    Preconditions.checkState(mayBeStatement(stmt));
    Node block = new Node(Token.BLOCK, stmt);
    return block;
  }

  public static Node block(Node ... stmts) {
    Node block = block();
    for (Node stmt : stmts) {
      Preconditions.checkState(mayBeStatement(stmt));
      block.addChildToBack(stmt);
    }
    return block;
  }

  public static Node block(List<Node> stmts) {
    Node paramList = block();
    for (Node stmt : stmts) {
      Preconditions.checkState(mayBeStatement(stmt));
      paramList.addChildToBack(stmt);
    }
    return paramList;
  }

  private static Node blockUnchecked(Node stmt) {
    return new Node(Token.BLOCK, stmt);
  }

  public static Node script() {
    // TODO(johnlenz): finish setting up the SCRIPT node
    Node block = new Node(Token.SCRIPT);
    return block;
  }

  public static Node script(Node ... stmts) {
    Node block = script();
    for (Node stmt : stmts) {
      Preconditions.checkState(mayBeStatementNoReturn(stmt));
      block.addChildToBack(stmt);
    }
    return block;
  }

  public static Node script(List<Node> stmts) {
    Node paramList = script();
    for (Node stmt : stmts) {
      Preconditions.checkState(mayBeStatementNoReturn(stmt));
      paramList.addChildToBack(stmt);
    }
    return paramList;
  }

  public static Node var(Node lhs, Node value) {
    return declaration(lhs, value, Token.VAR);
  }

  public static Node let(Node lhs, Node value) {
    return declaration(lhs, value, Token.LET);
  }

  public static Node constNode(Node lhs, Node value) {
    return declaration(lhs, value, Token.CONST);
  }

  public static Node var(Node lhs) {
    return declaration(lhs, Token.VAR);
  }

  public static Node declaration(Node lhs, int type) {
    Preconditions.checkState(
        lhs.isName() || lhs.isDestructuringPattern() || lhs.isDestructuringLhs(),
        lhs);
    if (lhs.isDestructuringPattern()) {
      lhs = new Node(Token.DESTRUCTURING_LHS, lhs);
    }
    return new Node(type, lhs);
  }

  public static Node declaration(Node lhs, Node value, int type) {
    if (lhs.isName()) {
      Preconditions.checkState(!lhs.hasChildren());
    } else {
      Preconditions.checkState(lhs.isArrayPattern() || lhs.isObjectPattern());
      lhs = new Node(Token.DESTRUCTURING_LHS, lhs);
    }
    Preconditions.checkState(mayBeExpression(value),
        "%s can't be an expression", value);

    lhs.addChildToBack(value);
    return new Node(type, lhs);
  }

  public static Node returnNode() {
    return new Node(Token.RETURN);
  }

  public static Node returnNode(Node expr) {
    Preconditions.checkState(mayBeExpression(expr));
    return new Node(Token.RETURN, expr);
  }

  public static Node yield() {
    return new Node(Token.YIELD);
  }

  public static Node yield(Node expr) {
    Preconditions.checkState(mayBeExpression(expr));
    return new Node(Token.YIELD, expr);
  }

  public static Node throwNode(Node expr) {
    Preconditions.checkState(mayBeExpression(expr));
    return new Node(Token.THROW, expr);
  }

  public static Node exprResult(Node expr) {
    Preconditions.checkState(mayBeExpression(expr));
    return new Node(Token.EXPR_RESULT, expr);
  }

  public static Node ifNode(Node cond, Node then) {
    Preconditions.checkState(mayBeExpression(cond));
    Preconditions.checkState(then.isBlock());
    return new Node(Token.IF, cond, then);
  }

  public static Node ifNode(Node cond, Node then, Node elseNode) {
    Preconditions.checkState(mayBeExpression(cond));
    Preconditions.checkState(then.isBlock());
    Preconditions.checkState(elseNode.isBlock());
    return new Node(Token.IF, cond, then, elseNode);
  }

  public static Node doNode(Node body, Node cond) {
    Preconditions.checkState(body.isBlock());
    Preconditions.checkState(mayBeExpression(cond));
    return new Node(Token.DO, body, cond);
  }

  public static Node whileNode(Node cond, Node body) {
    Preconditions.checkState(body.isBlock());
    Preconditions.checkState(mayBeExpression(cond));
    return new Node(Token.WHILE, cond, body);
  }

  public static Node forIn(Node target, Node cond, Node body) {
    Preconditions.checkState(target.isVar() || mayBeExpression(target));
    Preconditions.checkState(mayBeExpression(cond));
    Preconditions.checkState(body.isBlock());
    return new Node(Token.FOR, target, cond, body);
  }

  public static Node forNode(Node init, Node cond, Node incr, Node body) {
    Preconditions.checkState(init.isVar() || mayBeExpressionOrEmpty(init));
    Preconditions.checkState(mayBeExpressionOrEmpty(cond));
    Preconditions.checkState(mayBeExpressionOrEmpty(incr));
    Preconditions.checkState(body.isBlock());
    return new Node(Token.FOR, init, cond, incr, body);
  }

  public static Node switchNode(Node cond, Node ... cases) {
    Preconditions.checkState(mayBeExpression(cond));
    Node switchNode = new Node(Token.SWITCH, cond);
    for (Node caseNode : cases) {
      Preconditions.checkState(caseNode.isCase() || caseNode.isDefaultCase());
      switchNode.addChildToBack(caseNode);
    }
    return switchNode;
  }

  public static Node caseNode(Node expr, Node body) {
    Preconditions.checkState(mayBeExpression(expr));
    Preconditions.checkState(body.isBlock());
    body.putBooleanProp(Node.SYNTHETIC_BLOCK_PROP, true);
    return new Node(Token.CASE, expr, body);
  }

  public static Node defaultCase(Node body) {
    Preconditions.checkState(body.isBlock());
    body.putBooleanProp(Node.SYNTHETIC_BLOCK_PROP, true);
    return new Node(Token.DEFAULT_CASE, body);
  }

  public static Node label(Node name, Node stmt) {
    // TODO(johnlenz): additional validation here.
    Preconditions.checkState(name.isLabelName());
    Preconditions.checkState(mayBeStatement(stmt));
    Node block = new Node(Token.LABEL, name, stmt);
    return block;
  }

  public static Node labelName(String name) {
    Preconditions.checkState(!name.isEmpty());
    return Node.newString(Token.LABEL_NAME, name);
  }

  public static Node tryFinally(Node tryBody, Node finallyBody) {
    Preconditions.checkState(tryBody.isBlock());
    Preconditions.checkState(finallyBody.isBlock());
    Node catchBody = block().useSourceInfoIfMissingFrom(tryBody);
    return new Node(Token.TRY, tryBody, catchBody, finallyBody);
  }

  public static Node tryCatch(Node tryBody, Node catchNode) {
    Preconditions.checkState(tryBody.isBlock());
    Preconditions.checkState(catchNode.isCatch());
    Node catchBody = blockUnchecked(catchNode).useSourceInfoIfMissingFrom(catchNode);
    return new Node(Token.TRY, tryBody, catchBody);
  }

  public static Node tryCatchFinally(
      Node tryBody, Node catchNode, Node finallyBody) {
    Preconditions.checkState(finallyBody.isBlock());
    Node tryNode = tryCatch(tryBody, catchNode);
    tryNode.addChildToBack(finallyBody);
    return tryNode;
  }

  public static Node catchNode(Node expr, Node body) {
    Preconditions.checkState(expr.isName());
    Preconditions.checkState(body.isBlock());
    return new Node(Token.CATCH, expr, body);
  }

  public static Node breakNode() {
    return new Node(Token.BREAK);
  }

  public static Node breakNode(Node name) {
    // TODO(johnlenz): additional validation here.
    Preconditions.checkState(name.isLabelName());
    return new Node(Token.BREAK, name);
  }

  public static Node continueNode() {
    return new Node(Token.CONTINUE);
  }

  public static Node continueNode(Node name) {
    // TODO(johnlenz): additional validation here.
    Preconditions.checkState(name.isLabelName());
    return new Node(Token.CONTINUE, name);
  }

  public static Node call(Node target, Node ... args) {
    Node call = new Node(Token.CALL, target);
    for (Node arg : args) {
      Preconditions.checkState(mayBeExpression(arg), arg);
      call.addChildToBack(arg);
    }
    return call;
  }

  public static Node newNode(Node target, Node ... args) {
    Node newcall = new Node(Token.NEW, target);
    for (Node arg : args) {
      Preconditions.checkState(mayBeExpression(arg));
      newcall.addChildToBack(arg);
    }
    return newcall;
  }

  public static Node name(String name) {
    Preconditions.checkState(name.indexOf('.') == -1,
        "Invalid name. Did you mean to use NodeUtil.newQName?");
    return Node.newString(Token.NAME, name);
  }

  public static Node getprop(Node target, Node prop) {
    Preconditions.checkState(mayBeExpression(target));
    Preconditions.checkState(prop.isString());
    return new Node(Token.GETPROP, target, prop);
  }

  public static Node getprop(Node target, Node prop, Node ...moreProps) {
    Preconditions.checkState(mayBeExpression(target));
    Preconditions.checkState(prop.isString());
    Node result = new Node(Token.GETPROP, target, prop);
    for (Node moreProp : moreProps) {
      Preconditions.checkState(moreProp.isString());
      result = new Node(Token.GETPROP, result, moreProp);
    }
    return result;
  }

  public static Node getprop(Node target, String prop, String ...moreProps) {
    Preconditions.checkState(mayBeExpression(target));
    Node result = new Node(Token.GETPROP, target, IR.string(prop));
    for (String moreProp : moreProps) {
      result = new Node(Token.GETPROP, result, IR.string(moreProp));
    }
    return result;
  }

  public static Node getelem(Node target, Node elem) {
    Preconditions.checkState(mayBeExpression(target));
    Preconditions.checkState(mayBeExpression(elem));
    return new Node(Token.GETELEM, target, elem);
  }

  public static Node assign(Node target, Node expr) {
    Preconditions.checkState(target.isValidAssignmentTarget(), target);
    Preconditions.checkState(mayBeExpression(expr), expr);
    return new Node(Token.ASSIGN, target, expr);
  }

  public static Node hook(Node cond, Node trueval, Node falseval) {
    Preconditions.checkState(mayBeExpression(cond));
    Preconditions.checkState(mayBeExpression(trueval));
    Preconditions.checkState(mayBeExpression(falseval));
    return new Node(Token.HOOK, cond, trueval, falseval);
  }

  public static Node in(Node expr1, Node expr2) {
    return binaryOp(Token.IN, expr1, expr2);
  }

  public static Node comma(Node expr1, Node expr2) {
    return binaryOp(Token.COMMA, expr1, expr2);
  }

  public static Node and(Node expr1, Node expr2) {
    return binaryOp(Token.AND, expr1, expr2);
  }

  public static Node or(Node expr1, Node expr2) {
    return binaryOp(Token.OR, expr1, expr2);
  }

  public static Node not(Node expr1) {
    return unaryOp(Token.NOT, expr1);
  }

  /**
   * "&lt;"
   */
  public static Node lt(Node expr1, Node expr2) {
    return binaryOp(Token.LT, expr1, expr2);
  }

  /**
   * "=="
   */
  public static Node eq(Node expr1, Node expr2) {
    return binaryOp(Token.EQ, expr1, expr2);
  }

  /**
   * "!="
   */
  public static Node ne(Node expr1, Node expr2) {
    return binaryOp(Token.NE, expr1, expr2);
  }

  /**
   * "==="
   */
  public static Node sheq(Node expr1, Node expr2) {
    return binaryOp(Token.SHEQ, expr1, expr2);
  }

  /**
   * "!=="
   */
  public static Node shne(Node expr1, Node expr2) {
    return binaryOp(Token.SHNE, expr1, expr2);
  }

  public static Node voidNode(Node expr1) {
    return unaryOp(Token.VOID, expr1);
  }

  public static Node neg(Node expr1) {
    return unaryOp(Token.NEG, expr1);
  }

  public static Node pos(Node expr1) {
    return unaryOp(Token.POS, expr1);
  }

  public static Node cast(Node expr1, JSDocInfo jsdoc) {
    Node op = unaryOp(Token.CAST, expr1);
    op.setJSDocInfo(jsdoc);
    return op;
  }

  public static Node inc(Node exp, boolean isPost) {
    Node op = unaryOp(Token.INC, exp);
    op.putBooleanProp(Node.INCRDECR_PROP, isPost);
    return op;
  }

  public static Node dec(Node exp, boolean isPost) {
    Node op = unaryOp(Token.DEC, exp);
    op.putBooleanProp(Node.INCRDECR_PROP, isPost);
    return op;
  }

  public static Node add(Node expr1, Node expr2) {
    return binaryOp(Token.ADD, expr1, expr2);
  }

  public static Node sub(Node expr1, Node expr2) {
    return binaryOp(Token.SUB, expr1, expr2);
  }

  // TODO(johnlenz): the rest of the ops

  // literals
  public static Node objectlit(Node ... propdefs) {
    Node objectlit = new Node(Token.OBJECTLIT);
    for (Node propdef : propdefs) {
      Preconditions.checkState(
          propdef.isStringKey() || propdef.isMemberFunctionDef() ||
          propdef.isGetterDef() || propdef.isSetterDef());
      if (!propdef.isStringKey()) {
        Preconditions.checkState(propdef.hasOneChild());
      }
      objectlit.addChildToBack(propdef);
    }
    return objectlit;
  }

  public static Node computedProp(Node key, Node value) {
    Preconditions.checkState(mayBeExpression(key), key);
    Preconditions.checkState(mayBeExpression(value), value);
    return new Node(Token.COMPUTED_PROP, key, value);
  }

  // TODO(johnlenz): quoted props

  public static Node propdef(Node string, Node value) {
    Preconditions.checkState(string.isStringKey());
    Preconditions.checkState(!string.hasChildren());
    Preconditions.checkState(mayBeExpression(value));
    string.addChildToFront(value);
    return string;
  }

  public static Node arraylit(Node ... exprs) {
    Node arraylit = new Node(Token.ARRAYLIT);
    for (Node expr : exprs) {
      Preconditions.checkState(mayBeExpressionOrEmpty(expr));
      arraylit.addChildToBack(expr);
    }
    return arraylit;
  }

  public static Node regexp(Node expr) {
    Preconditions.checkState(expr.isString());
    return new Node(Token.REGEXP, expr);
  }

  public static Node regexp(Node expr, Node flags) {
    Preconditions.checkState(expr.isString());
    Preconditions.checkState(flags.isString());
    return new Node(Token.REGEXP, expr, flags);
  }

  public static Node string(String s) {
    return Node.newString(s);
  }

  public static Node stringKey(String s) {
    return Node.newString(Token.STRING_KEY, s);
  }

  public static Node stringKey(String s, Node value) {
    Preconditions.checkState(mayBeExpression(value));
    Node stringKey = stringKey(s);
    stringKey.addChildToFront(value);
    return stringKey;
  }

  public static Node rest(String name) {
    return new Node(Token.REST, name(name));
  }

  public static Node spread(Node expr) {
    Preconditions.checkState(mayBeExpression(expr));
    return new Node(Token.SPREAD, expr);
  }

  public static Node superNode() {
    return new Node(Token.SUPER);
  }

  public static Node memberFunctionDef(String name, Node function) {
    Preconditions.checkState(function.isFunction());
    Node member = Node.newString(Token.MEMBER_FUNCTION_DEF, name);
    member.addChildToBack(function);
    return member;
  }

  public static Node number(double d) {
    return Node.newNumber(d);
  }

  public static Node thisNode() {
    return new Node(Token.THIS);
  }

  public static Node trueNode() {
    return new Node(Token.TRUE);
  }

  public static Node falseNode() {
    return new Node(Token.FALSE);
  }

  public static Node nullNode() {
    return new Node(Token.NULL);
  }

  // helper methods

  private static Node binaryOp(int token, Node expr1, Node expr2) {
    Preconditions.checkState(mayBeExpression(expr1), expr1);
    Preconditions.checkState(mayBeExpression(expr2), expr2);
    return new Node(token, expr1, expr2);
  }

  private static Node unaryOp(int token, Node expr) {
    Preconditions.checkState(mayBeExpression(expr));
    return new Node(token, expr);
  }

  private static boolean mayBeExpressionOrEmpty(Node n) {
    return n.isEmpty() || mayBeExpression(n);
  }

  // NOTE: some nodes are neither statements nor expression nodes:
  //   SCRIPT, LABEL_NAME, PARAM_LIST, CASE, DEFAULT_CASE, CATCH
  //   GETTER_DEF, SETTER_DEF

  /**
   * It isn't possible to always determine if a detached node is a expression,
   * so make a best guess.
   */
  private static boolean mayBeStatementNoReturn(Node n) {
    switch (n.getType()) {
      case Token.EMPTY:
      case Token.FUNCTION:
        // EMPTY and FUNCTION are used both in expression and statement
        // contexts
        return true;

      case Token.BLOCK:
      case Token.BREAK:
      case Token.CONST:
      case Token.CONTINUE:
      case Token.DEBUGGER:
      case Token.DO:
      case Token.EXPR_RESULT:
      case Token.FOR:
      case Token.IF:
      case Token.LABEL:
      case Token.SWITCH:
      case Token.THROW:
      case Token.TRY:
      case Token.VAR:
      case Token.WHILE:
      case Token.WITH:
        return true;

      default:
        return false;
    }
  }

  /**
   * It isn't possible to always determine if a detached node is a expression,
   * so make a best guess.
   */
  private static boolean mayBeStatement(Node n) {
    if (!mayBeStatementNoReturn(n)) {
      return n.isReturn();
    }
    return true;
  }

  /**
   * It isn't possible to always determine if a detached node is a expression,
   * so make a best guess.
   */
  private static boolean mayBeExpression(Node n) {
    switch (n.getType()) {
      case Token.FUNCTION:
      case Token.CLASS:
        // FUNCTION and CLASS are used both in expression and statement
        // contexts.
        return true;

      case Token.ADD:
      case Token.AND:
      case Token.ARRAYLIT:
      case Token.ASSIGN:
      case Token.ASSIGN_BITOR:
      case Token.ASSIGN_BITXOR:
      case Token.ASSIGN_BITAND:
      case Token.ASSIGN_LSH:
      case Token.ASSIGN_RSH:
      case Token.ASSIGN_URSH:
      case Token.ASSIGN_ADD:
      case Token.ASSIGN_SUB:
      case Token.ASSIGN_MUL:
      case Token.ASSIGN_DIV:
      case Token.ASSIGN_MOD:
      case Token.BITAND:
      case Token.BITOR:
      case Token.BITNOT:
      case Token.BITXOR:
      case Token.CALL:
      case Token.CAST:
      case Token.COMMA:
      case Token.DEC:
      case Token.DELPROP:
      case Token.DIV:
      case Token.EQ:
      case Token.FALSE:
      case Token.GE:
      case Token.GETPROP:
      case Token.GETELEM:
      case Token.GT:
      case Token.HOOK:
      case Token.IN:
      case Token.INC:
      case Token.INSTANCEOF:
      case Token.LE:
      case Token.LSH:
      case Token.LT:
      case Token.MOD:
      case Token.MUL:
      case Token.NAME:
      case Token.NE:
      case Token.NEG:
      case Token.NEW:
      case Token.NOT:
      case Token.NUMBER:
      case Token.NULL:
      case Token.OBJECTLIT:
      case Token.OR:
      case Token.POS:
      case Token.REGEXP:
      case Token.RSH:
      case Token.SHEQ:
      case Token.SHNE:
      case Token.SPREAD:
      case Token.STRING:
      case Token.SUB:
      case Token.SUPER:
      case Token.TEMPLATELIT:
      case Token.TAGGED_TEMPLATELIT:
      case Token.THIS:
      case Token.TYPEOF:
      case Token.TRUE:
      case Token.URSH:
      case Token.VOID:
      case Token.YIELD:
        return true;

      default:
        return false;
    }
  }
}
