/*
 * Copyright 2008 Google Inc.
 *
 * 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.
 */
package com.google.gwt.dev.jjs.impl;

import com.google.gwt.dev.jjs.ast.CanBeAbstract;
import com.google.gwt.dev.jjs.ast.CanBeFinal;
import com.google.gwt.dev.jjs.ast.CanBeStatic;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.HasName;
import com.google.gwt.dev.jjs.ast.HasType;
import com.google.gwt.dev.jjs.ast.JArrayLength;
import com.google.gwt.dev.jjs.ast.JArrayRef;
import com.google.gwt.dev.jjs.ast.JArrayType;
import com.google.gwt.dev.jjs.ast.JAssertStatement;
import com.google.gwt.dev.jjs.ast.JBinaryOperation;
import com.google.gwt.dev.jjs.ast.JBlock;
import com.google.gwt.dev.jjs.ast.JBooleanLiteral;
import com.google.gwt.dev.jjs.ast.JBreakStatement;
import com.google.gwt.dev.jjs.ast.JCaseStatement;
import com.google.gwt.dev.jjs.ast.JCastMap;
import com.google.gwt.dev.jjs.ast.JCastOperation;
import com.google.gwt.dev.jjs.ast.JCharLiteral;
import com.google.gwt.dev.jjs.ast.JClassLiteral;
import com.google.gwt.dev.jjs.ast.JConditional;
import com.google.gwt.dev.jjs.ast.JConstructor;
import com.google.gwt.dev.jjs.ast.JContinueStatement;
import com.google.gwt.dev.jjs.ast.JDeclarationStatement;
import com.google.gwt.dev.jjs.ast.JDeclaredType;
import com.google.gwt.dev.jjs.ast.JDoStatement;
import com.google.gwt.dev.jjs.ast.JDoubleLiteral;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JExpressionStatement;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JFieldRef;
import com.google.gwt.dev.jjs.ast.JFloatLiteral;
import com.google.gwt.dev.jjs.ast.JForStatement;
import com.google.gwt.dev.jjs.ast.JIfStatement;
import com.google.gwt.dev.jjs.ast.JInstanceOf;
import com.google.gwt.dev.jjs.ast.JIntLiteral;
import com.google.gwt.dev.jjs.ast.JLabel;
import com.google.gwt.dev.jjs.ast.JLabeledStatement;
import com.google.gwt.dev.jjs.ast.JLocal;
import com.google.gwt.dev.jjs.ast.JLocalRef;
import com.google.gwt.dev.jjs.ast.JLongLiteral;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethodBody;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JNameOf;
import com.google.gwt.dev.jjs.ast.JNewArray;
import com.google.gwt.dev.jjs.ast.JNewInstance;
import com.google.gwt.dev.jjs.ast.JNode;
import com.google.gwt.dev.jjs.ast.JNullLiteral;
import com.google.gwt.dev.jjs.ast.JParameter;
import com.google.gwt.dev.jjs.ast.JParameterRef;
import com.google.gwt.dev.jjs.ast.JPermutationDependentValue;
import com.google.gwt.dev.jjs.ast.JPostfixOperation;
import com.google.gwt.dev.jjs.ast.JPrefixOperation;
import com.google.gwt.dev.jjs.ast.JPrimitiveType;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JReferenceType;
import com.google.gwt.dev.jjs.ast.JReturnStatement;
import com.google.gwt.dev.jjs.ast.JRuntimeTypeReference;
import com.google.gwt.dev.jjs.ast.JStatement;
import com.google.gwt.dev.jjs.ast.JStringLiteral;
import com.google.gwt.dev.jjs.ast.JSwitchExpression;
import com.google.gwt.dev.jjs.ast.JSwitchStatement;
import com.google.gwt.dev.jjs.ast.JThisRef;
import com.google.gwt.dev.jjs.ast.JThrowStatement;
import com.google.gwt.dev.jjs.ast.JTryStatement;
import com.google.gwt.dev.jjs.ast.JType;
import com.google.gwt.dev.jjs.ast.JWhileStatement;
import com.google.gwt.dev.jjs.ast.JYieldStatement;
import com.google.gwt.dev.jjs.ast.js.JDebuggerStatement;
import com.google.gwt.dev.jjs.ast.js.JMultiExpression;
import com.google.gwt.dev.jjs.ast.js.JsniFieldRef;
import com.google.gwt.dev.jjs.ast.js.JsniMethodBody;
import com.google.gwt.dev.jjs.ast.js.JsniMethodRef;
import com.google.gwt.dev.jjs.ast.js.JsonArray;
import com.google.gwt.dev.js.JsSourceGenerationVisitor;
import com.google.gwt.dev.util.TextOutput;

import java.util.Iterator;

/**
 * Implements a reasonable toString() for all JNodes. The goal is to print a
 * recognizable declaration for large constructs (classes, methods) for easy use
 * in a debugger. Expressions and Statements should look like Java code
 * fragments.
 */
public class ToStringGenerationVisitor extends TextOutputVisitor {

  protected static final char[] CHARS_ABSTRACT = "abstract ".toCharArray();
  protected static final char[] CHARS_ASSERT = "assert ".toCharArray();
  protected static final char[] CHARS_BREAK = "break".toCharArray();
  protected static final char[] CHARS_CASE = "case ".toCharArray();
  protected static final char[] CHARS_CATCH = " catch ".toCharArray();
  protected static final char[] CHARS_CLASS = "class ".toCharArray();
  protected static final char[] CHARS_COMMA = ", ".toCharArray();
  protected static final char[] CHARS_CONTINUE = "continue".toCharArray();
  protected static final char[] CHARS_DEFAULT = "default".toCharArray();
  protected static final char[] CHARS_DO = "do".toCharArray();
  protected static final char[] CHARS_DOTCLASS = ".class".toCharArray();
  protected static final char[] CHARS_ELSE = "else".toCharArray();
  protected static final char[] CHARS_EXTENDS = "extends ".toCharArray();
  protected static final char[] CHARS_FALSE = "false".toCharArray();
  protected static final char[] CHARS_FINAL = "final ".toCharArray();
  protected static final char[] CHARS_FINALLY = " finally ".toCharArray();
  protected static final char[] CHARS_FOR = "for ".toCharArray();
  protected static final char[] CHARS_IF = "if ".toCharArray();
  protected static final char[] CHARS_IMPLEMENTS = "implements ".toCharArray();
  protected static final char[] CHARS_INSTANCEOF = " instanceof ".toCharArray();
  protected static final char[] CHARS_INTERFACE = "interface ".toCharArray();
  protected static final char[] CHARS_NAMEOF = " JNameOf ".toCharArray();
  protected static final char[] CHARS_NATIVE = "native ".toCharArray();
  protected static final char[] CHARS_NEW = "new ".toCharArray();
  protected static final char[] CHARS_NULL = "null".toCharArray();
  protected static final char[] CHARS_PIPE = " | ".toCharArray();
  protected static final char[] CHARS_PRIVATE = "private ".toCharArray();
  protected static final char[] CHARS_PROTECTED = "protected ".toCharArray();
  protected static final char[] CHARS_PUBLIC = "public ".toCharArray();
  protected static final char[] CHARS_RETURN = "return".toCharArray();
  protected static final char[] CHARS_RUNTIMETYPEREFERENCE =
      " JRuntimeTypeReference ".toCharArray();
  protected static final char[] CHARS_SLASHSTAR = "/*".toCharArray();
  protected static final char[] CHARS_STARSLASH = "*/".toCharArray();
  protected static final char[] CHARS_STATIC = "static ".toCharArray();
  protected static final char[] CHARS_SUPER = "super".toCharArray();
  protected static final char[] CHARS_SWITCH = "switch ".toCharArray();
  protected static final char[] CHARS_THIS = "this".toCharArray();
  protected static final char[] CHARS_THROW = "throw".toCharArray();
  protected static final char[] CHARS_TRUE = "true".toCharArray();
  protected static final char[] CHARS_TRY = "try ".toCharArray();
  protected static final char[] CHARS_WHILE = "while ".toCharArray();
  protected static final char[] CHARS_YIELD = "yield ".toCharArray();

  private boolean needSemi = true;

  private boolean suppressType = false;

  public ToStringGenerationVisitor(TextOutput textOutput) {
    super(textOutput);
  }

  @Override
  public boolean visit(JArrayLength x, Context ctx) {
    JExpression instance = x.getInstance();
    parenPush(x, instance);
    accept(instance);
    parenPop(x, instance);
    print(".length");
    return false;
  }

  @Override
  public boolean visit(JArrayRef x, Context ctx) {
    JExpression instance = x.getInstance();
    parenPush(x, instance);
    accept(instance);
    parenPop(x, instance);
    print('[');
    accept(x.getIndexExpr());
    print(']');
    return false;
  }

  @Override
  public boolean visit(JArrayType x, Context ctx) {
    accept(x.getElementType());
    print("[]");
    return false;
  }

  @Override
  public boolean visit(JAssertStatement x, Context ctx) {
    print(CHARS_ASSERT);
    accept(x.getTestExpr());
    if (x.getArg() != null) {
      print(" : ");
      accept(x.getArg());
    }
    return false;
  }

  @Override
  public boolean visit(JBinaryOperation x, Context ctx) {
    // TODO(later): associativity
    JExpression arg1 = x.getLhs();
    parenPush(x, arg1);
    accept(arg1);
    parenPop(x, arg1);

    space();
    print(x.getOp().getSymbol());
    space();

    JExpression arg2 = x.getRhs();
    parenPush(x, arg2);
    accept(arg2);
    parenPop(x, arg2);

    return false;
  }

  @Override
  public boolean visit(JBlock x, Context ctx) {
    openBlock();
    for (int i = 0; i < x.getStatements().size(); ++i) {
      JStatement statement = x.getStatements().get(i);
      needSemi = true;
      accept(statement);
      if (needSemi) {
        semi();
      }
      newline();
    }
    closeBlock();
    needSemi = false;
    return false;
  }

  @Override
  public boolean visit(JBooleanLiteral x, Context ctx) {
    printBooleanLiteral(x.getValue());
    return false;
  }

  @Override
  public boolean visit(JBreakStatement x, Context ctx) {
    print(CHARS_BREAK);
    if (x.getLabel() != null) {
      space();
      accept(x.getLabel());
    }
    return false;
  }

  @Override
  public boolean visit(JCaseStatement x, Context ctx) {
    if (x.isDefault()) {
      print(CHARS_DEFAULT);
    } else {
      print(CHARS_CASE);
      boolean first = true;
      for (JExpression expr : x.getExprs()) {
        if (!first) {
          print(',');
          space();
        }
        first = false;
        accept(expr);
      }
    }
    print(':');
    space();
    needSemi = false;
    return false;
  }

  @Override
  public boolean visit(JCastOperation x, Context ctx) {
    lparen();
    printType(x);
    rparen();
    space();

    JExpression expr = x.getExpr();
    parenPush(x, expr);
    accept(expr);
    parenPop(x, expr);
    return false;
  }

  @Override
  public boolean visit(JCastMap x, Context ctx) {
    print('[');
    visitCollectionWithCommas(x.getCanCastToTypes().iterator());
    print(']');
    return false;
  }

  @Override
  public boolean visit(JCharLiteral x, Context ctx) {
    printCharLiteral(x.getValue());
    return false;
  }

  @Override
  public boolean visit(JClassLiteral x, Context ctx) {
    printTypeName(x.getRefType());
    print(CHARS_DOTCLASS);
    return false;
  }

  @Override
  public boolean visit(JType x, Context ctx) {
    print(x.getDescription());
    return false;
  }

  @Override
  public boolean visit(JDeclaredType x, Context ctx) {
    visit((JType) x, ctx);
    print(" (");
    printAbstractFlag(x);
    printFinalFlag(x);
    if (x.getSuperClass() != null) {
      print(CHARS_EXTENDS);
      printTypeName(x.getSuperClass());
    }

    if (x.getImplements().size() > 0) {
      space();
      print(CHARS_IMPLEMENTS);
      for (int i = 0, c = x.getImplements().size(); i < c; ++i) {
        if (i > 0) {
          print(CHARS_COMMA);
        }
        printTypeName(x.getImplements().get(i));
      }
    }
    print(")");
    return false;
  }

  @Override
  public boolean visit(JConditional x, Context ctx) {
    // TODO(later): associativity
    JExpression ifTest = x.getIfTest();
    parenPush(x, ifTest);
    accept(ifTest);
    parenPop(x, ifTest);

    print(" ? ");

    JExpression thenExpr = x.getThenExpr();
    parenPush(x, thenExpr);
    accept(thenExpr);
    parenPop(x, thenExpr);

    print(" : ");

    JExpression elseExpr = x.getElseExpr();
    parenPush(x, elseExpr);
    accept(elseExpr);
    parenPop(x, elseExpr);

    return false;
  }

  @Override
  public boolean visit(JConstructor x, Context ctx) {
    // Modifiers
    if (x.isPrivate()) {
      print(CHARS_PRIVATE);
    } else {
      print(CHARS_PUBLIC);
    }
    printName(x);

    // Parameters
    printParameterList(x);

    if (x.isAbstract() || !shouldPrintMethodBody()) {
      semi();
      newlineOpt();
    } else {
      accept(x.getBody());
    }

    return false;
  }

  @Override
  public boolean visit(JContinueStatement x, Context ctx) {
    print(CHARS_CONTINUE);
    if (x.getLabel() != null) {
      space();
      accept(x.getLabel());
    }
    return false;
  }

  @Override
  public boolean visit(JDebuggerStatement x, Context ctx) {
    print("GWT.debugger()");
    return false;
  }

  @Override
  public boolean visit(JDeclarationStatement x, Context ctx) {
    if (!suppressType) {
      accept(x.getVariableRef().getTarget());
    } else {
      accept(x.getVariableRef());
    }
    JExpression initializer = x.getInitializer();
    if (initializer != null) {
      print(" = ");
      accept(initializer);
    }
    return false;
  }

  @Override
  public boolean visit(JDoStatement x, Context ctx) {
    print(CHARS_DO);
    needSemi = true;
    if (x.getBody() != null) {
      nestedStatementPush(x.getBody());
      accept(x.getBody());
      nestedStatementPop(x.getBody());
    }
    if (needSemi) {
      semi();
      newline();
    } else {
      space();
      needSemi = true;
    }
    print(CHARS_WHILE);
    lparen();
    accept(x.getTestExpr());
    rparen();
    return false;
  }

  @Override
  public boolean visit(JDoubleLiteral x, Context ctx) {
    printDoubleLiteral(x.getValue());
    return false;
  }

  @Override
  public boolean visit(JExpressionStatement x, Context ctx) {
    accept(x.getExpr());
    return false;
  }

  @Override
  public boolean visit(JField x, Context ctx) {
    printFinalFlag(x);
    printStaticFlag(x);
    printType(x);
    space();
    printName(x);
    return false;
  }

  @Override
  public boolean visit(JFieldRef x, Context ctx) {
    JExpression instance = x.getInstance();
    if (instance != null) {
      parenPush(x, instance);
      accept(instance);
      parenPop(x, instance);
    } else {
      printTypeName(x.getField().getEnclosingType());
    }
    print('.');
    printName(x.getField());
    return false;
  }

  @Override
  public boolean visit(JFloatLiteral x, Context ctx) {
    printDoubleLiteral(x.getValue());
    return false;
  }

  @Override
  public boolean visit(JForStatement x, Context ctx) {
    print(CHARS_FOR);
    lparen();

    Iterator<JStatement> iter = x.getInitializers().iterator();
    if (iter.hasNext()) {
      JStatement stmt = iter.next();
      accept(stmt);
    }
    suppressType = true;
    while (iter.hasNext()) {
      print(CHARS_COMMA);
      JStatement stmt = iter.next();
      accept(stmt);
    }
    suppressType = false;

    semi();
    space();
    if (x.getCondition() != null) {
      accept(x.getCondition());
    }

    semi();
    space();
    if (x.getIncrements() != null) {
      accept(x.getIncrements());
    }
    rparen();

    if (x.getBody() != null) {
      nestedStatementPush(x.getBody());
      accept(x.getBody());
      nestedStatementPop(x.getBody());
    }
    return false;
  }

  @Override
  public boolean visit(JIfStatement x, Context ctx) {
    print(CHARS_IF);
    lparen();
    accept(x.getIfExpr());
    rparen();

    if (x.getThenStmt() != null) {
      nestedStatementPush(x.getThenStmt());
      accept(x.getThenStmt());
      nestedStatementPop(x.getThenStmt());
    }

    if (x.getElseStmt() != null) {
      if (needSemi) {
        semi();
        newline();
      } else {
        space();
        needSemi = true;
      }
      print(CHARS_ELSE);
      boolean elseIf = x.getElseStmt() instanceof JIfStatement;
      if (!elseIf) {
        nestedStatementPush(x.getElseStmt());
      } else {
        space();
      }
      accept(x.getElseStmt());
      if (!elseIf) {
        nestedStatementPop(x.getElseStmt());
      }
    }

    return false;
  }

  @Override
  public boolean visit(JInstanceOf x, Context ctx) {
    JExpression expr = x.getExpr();
    parenPush(x, expr);
    accept(expr);
    parenPop(x, expr);
    print(CHARS_INSTANCEOF);
    printTypeName(x.getTestType());
    return false;
  }

  @Override
  public boolean visit(JIntLiteral x, Context ctx) {
    print(Integer.toString(x.getValue()));
    return false;
  }

  @Override
  public boolean visit(JLabel x, Context ctx) {
    printName(x);
    return false;
  }

  @Override
  public boolean visit(JLabeledStatement x, Context ctx) {
    accept(x.getLabel());
    print(" : ");
    accept(x.getBody());
    return false;
  }

  @Override
  public boolean visit(JLocal x, Context ctx) {
    printFinalFlag(x);
    printType(x);
    space();
    printName(x);
    return false;
  }

  @Override
  public boolean visit(JLocalRef x, Context ctx) {
    printName(x.getLocal());
    return false;
  }

  @Override
  public boolean visit(JLongLiteral x, Context ctx) {
    printLongLiteral(x.getValue());
    return false;
  }

  @Override
  public boolean visit(JMethod x, Context ctx) {
    if (!shouldPrintMethodBody()) {
      print(x.getEnclosingType().getName() + "." + x.getSignature());
      return false;
    }
    printMethodHeader(x);
    if (x.isAbstract() || x.isJsNative()) {
      semi();
      newlineOpt();
    } else {
      accept(x.getBody());
    }
    return false;
  }

  @Override
  public boolean visit(JMethodBody x, Context ctx) {
    accept(x.getBlock());
    return false;
  }

  @Override
  public boolean visit(JMethodCall x, Context ctx) {
    JExpression instance = x.getInstance();
    JMethod target = x.getTarget();
    if (instance == null) {
      // Static call.
      printTypeName(target.getEnclosingType());
      print('.');
      printName(target);
    } else {
      // Instance call.
      parenPush(x, instance);
      accept(instance);
      parenPop(x, instance);
      print('.');
      if (x.isStaticDispatchOnly()) {
        printTypeName(target.getEnclosingType());
        print('.');
      }
      printName(target);
    }
    lparen();
    visitCollectionWithCommas(x.getArgs().iterator());
    rparen();
    return false;
  }

  @Override
  public boolean visit(JMultiExpression x, Context ctx) {
    lparen();
    visitCollectionWithCommas(x.getExpressions().iterator());
    rparen();
    return false;
  }

  @Override
  public boolean visit(JNameOf x, Context ctx) {
    print(CHARS_SLASHSTAR);
    print(CHARS_NAMEOF);
    print(CHARS_STARSLASH);
    printStringLiteral(x.getNode().getName());
    return false;
  }

  @Override
  public boolean visit(JRuntimeTypeReference x, Context ctx) {
    print(CHARS_SLASHSTAR);
    print(CHARS_RUNTIMETYPEREFERENCE);
    print(CHARS_STARSLASH);
    printStringLiteral(x.getReferredType().getName());
    return false;
  }

  @Override
  public boolean visit(JNewArray x, Context ctx) {
    print(CHARS_NEW);
    printTypeName(x.getArrayType());
    if (x.getInitializers() != null) {
      print(" {");
      visitCollectionWithCommas(x.getInitializers().iterator());
      print('}');
    } else {
      for (int i = 0; i < x.getDimensionExpressions().size(); ++i) {
        JExpression expr = x.getDimensionExpressions().get(i);
        print('[');
        accept(expr);
        print(']');
      }
    }

    return false;
  }

  @Override
  public boolean visit(JNewInstance x, Context ctx) {
    print(CHARS_NEW);
    JConstructor target = x.getTarget();
    printName(target);
    lparen();
    visitCollectionWithCommas(x.getArgs().iterator());
    rparen();
    return false;
  }

  @Override
  public boolean visit(JNullLiteral x, Context ctx) {
    print(CHARS_NULL);
    return false;
  }

  @Override
  public boolean visit(JParameter x, Context ctx) {
    printType(x);
    space();
    printName(x);
    return false;
  }

  @Override
  public boolean visit(JParameterRef x, Context ctx) {
    printName(x.getTarget());
    return false;
  }

  @Override
  public boolean visit(JPermutationDependentValue x, Context ctx) {
    if (x.isTypeRebind()) {
      print("GWT.create(");
      print(x.getRequestedValue());
      print(".class)");
    } else {
      assert x.isProperty();
      print("System.getProperty(");
      print("\"");
      print(x.getRequestedValue());
      print("\"");
      if (x.getDefaultValueExpression() != null) {
        print(", ");
        accept(x.getDefaultValueExpression());
      }
      print(")");
    }
    return false;
  }

  @Override
  public boolean visit(JPostfixOperation x, Context ctx) {
    // TODO(later): associativity
    JExpression arg = x.getArg();
    parenPush(x, arg);
    accept(arg);
    parenPop(x, arg);
    print(x.getOp().getSymbol());
    return false;
  }

  @Override
  public boolean visit(JPrefixOperation x, Context ctx) {
    // TODO(later): associativity
    print(x.getOp().getSymbol());
    JExpression arg = x.getArg();
    parenPush(x, arg);
    accept(arg);
    parenPop(x, arg);
    return false;
  }

  @Override
  public boolean visit(JPrimitiveType x, Context ctx) {
    printTypeName(x);
    return false;
  }

  @Override
  public boolean visit(JProgram x, Context ctx) {
    print("<JProgram>");
    return false;
  }

  @Override
  public boolean visit(JReturnStatement x, Context ctx) {
    print(CHARS_RETURN);
    if (x.getExpr() != null) {
      space();
      accept(x.getExpr());
    }
    return false;
  }

  @Override
  public boolean visit(JsniFieldRef x, Context ctx) {
    return visit(x.getField(), ctx);
  }

  @Override
  public boolean visit(final JsniMethodBody x, Context ctx) {
    print(" /*-");
    new JsSourceGenerationVisitor(this) {
      {
        printJsBlock(x.getFunc().getBody(), false, false);
      }
    };
    print("-*/");
    semi();
    return false;
  }

  @Override
  public boolean visit(JsniMethodRef x, Context ctx) {
    printMethodHeader(x.getTarget());
    return false;
  }

  @Override
  public boolean visit(JsonArray x, Context ctx) {
    print('[');
    visitCollectionWithCommas(x.getExpressions().iterator());
    print(']');
    return false;
  }

  @Override
  public boolean visit(JStringLiteral x, Context ctx) {
    printStringLiteral(x.getValue());
    return false;
  }

  @Override
  public boolean visit(JSwitchExpression x, Context ctx) {
    return writeSwitch(x.getExpr(), x.getBody());
  }

  private boolean writeSwitch(JExpression expr, JBlock body) {
    print(CHARS_SWITCH);
    lparen();
    accept(expr);
    rparen();
    space();
    nestedStatementPush(body);
    accept(body);
    nestedStatementPop(body);
    return false;
  }

  @Override
  public boolean visit(JSwitchStatement x, Context ctx) {
    return writeSwitch(x.getExpr(), x.getBody());
  }

  @Override
  public boolean visit(JThisRef x, Context ctx) {
    print(CHARS_THIS);
    return false;
  }

  @Override
  public boolean visit(JThrowStatement x, Context ctx) {
    print(CHARS_THROW);
    if (x.getExpr() != null) {
      space();
      accept(x.getExpr());
    }
    return false;
  }

  @Override
  public boolean visit(JTryStatement x, Context ctx) {
    print(CHARS_TRY);
    accept(x.getTryBlock());
    for (JTryStatement.CatchClause clause : x.getCatchClauses()) {
      print(CHARS_CATCH);
      lparen();

      Iterator<JType> it = clause.getTypes().iterator();
      printTypeName(it.next());
      while (it.hasNext()) {
        print(CHARS_PIPE);
        printTypeName(it.next());
      }
      space();

      printName(clause.getArg().getTarget());
      rparen();
      space();
      accept(clause.getBlock());
    }
    if (x.getFinallyBlock() != null) {
      print(CHARS_FINALLY);
      accept(x.getFinallyBlock());
    }
    return false;
  }

  @Override
  public boolean visit(JWhileStatement x, Context ctx) {
    print(CHARS_WHILE);
    lparen();
    accept(x.getTestExpr());
    rparen();
    if (x.getBody() != null) {
      nestedStatementPush(x.getBody());
      accept(x.getBody());
      nestedStatementPop(x.getBody());
    }
    return false;
  }

  @Override
  public boolean visit(JYieldStatement x, Context ctx) {
    print(CHARS_YIELD);
    space();
    accept(x.getExpr());
    return false;
  }

  protected void closeBlock() {
    indentOut();
    print('}');
  }

  protected void lparen() {
    print('(');
  }

  protected boolean nestedStatementPop(JStatement statement) {
    boolean pop = !(statement instanceof JBlock);
    if (pop) {
      indentOut();
    }
    return pop;
  }

  protected boolean nestedStatementPush(JStatement statement) {
    boolean push = !(statement instanceof JBlock);
    if (push) {
      indentIn();
      newline();
    } else {
      space();
    }
    return push;
  }

  protected void openBlock() {
    print('{');
    indentIn();
    newline();
  }

  protected boolean parenPop(int parentPrec, JExpression child) {
    int childPrec = JavaPrecedenceVisitor.exec(child);
    if (parentPrec < childPrec) {
      rparen();
      return true;
    } else {
      return false;
    }
  }

  protected boolean parenPop(JExpression parent, JExpression child) {
    return parenPop(JavaPrecedenceVisitor.exec(parent), child);
  }

  protected boolean parenPush(int parentPrec, JExpression child) {
    int childPrec = JavaPrecedenceVisitor.exec(child);
    if (parentPrec < childPrec) {
      lparen();
      return true;
    } else {
      return false;
    }
  }

  protected boolean parenPush(JExpression parent, JExpression child) {
    return parenPush(JavaPrecedenceVisitor.exec(parent), child);
  }

  protected void printAbstractFlag(CanBeAbstract x) {
    if (x.isAbstract()) {
      print(CHARS_ABSTRACT);
    }
  }

  protected void printBooleanLiteral(boolean value) {
    print(value ? CHARS_TRUE : CHARS_FALSE);
  }

  protected void printChar(char c) {
    switch (c) {
      case '\b':
        print("\\b");
        break;
      case '\t':
        print("\\t");
        break;
      case '\n':
        print("\\n");
        break;
      case '\f':
        print("\\f");
        break;
      case '\r':
        print("\\r");
        break;
      case '\"':
        print("\\\"");
        break;
      case '\'':
        print("\\'");
        break;
      case '\\':
        print("\\\\");
        break;
      default:
        if (Character.isISOControl(c)) {
          print("\\u");
          if (c < 0x1000) {
            print('0');
          }

          if (c < 0x100) {
            print('0');
          }

          if (c < 0x10) {
            print('0');
          }
          print(Integer.toHexString(c));
        } else {
          print(c);
        }
    }
  }

  protected void printCharLiteral(char value) {
    print('\'');
    printChar(value);
    print('\'');
  }

  protected void printDoubleLiteral(double value) {
    print(Double.toString(value));
  }

  protected void printFinalFlag(CanBeFinal x) {
    if (x.isFinal()) {
      print(CHARS_FINAL);
    }
  }

  protected void printLongLiteral(long value) {
    print(Long.toString(value));
    print('L');
  }

  protected void printMethodHeader(JMethod x) {
    // Modifiers
    switch (x.getAccess()) {
      case PUBLIC:
        print(CHARS_PUBLIC);
        break;
      case PROTECTED:
        print(CHARS_PROTECTED);
        break;
      case PRIVATE:
        print(CHARS_PRIVATE);
        break;
      case DEFAULT:
        break;
    }
    printStaticFlag(x);
    printAbstractFlag(x);
    printNativeFlag(x);
    printFinalFlag(x);
    printType(x);
    space();
    printName(x);

    // Parameters
    printParameterList(x);
  }

  protected void printName(HasName x) {
    print(x.getName());
  }

  protected void printNativeFlag(JMethod x) {
    if (x.isJsniMethod()) {
      print(CHARS_NATIVE);
    }
  }

  protected void printParameterList(JMethod x) {
    lparen();
    visitCollectionWithCommas(x.getParams().iterator());
    rparen();
  }

  protected void printStaticFlag(CanBeStatic x) {
    if (x.isStatic()) {
      print(CHARS_STATIC);
    }
  }

  protected void printStringLiteral(String string) {
    char[] s = string.toCharArray();
    print('\"');
    for (char value : s) {
      printChar(value);
    }
    print('\"');
  }

  protected void printType(HasType hasType) {
    printTypeName(hasType.getType());
  }

  protected void printTypeName(JType type) {
    if (type instanceof JReferenceType) {
      print(type.getShortName());
    } else {
      print(type.getName());
    }
  }

  protected void rparen() {
    print(')');
  }

  protected void semi() {
    print(';');
  }

  protected boolean shouldPrintMethodBody() {
    return false;
  }

  protected void space() {
    print(' ');
  }

  protected void visitCollectionWithCommas(Iterator<? extends JNode> iter) {
    visitCollectionWith(CHARS_COMMA, iter);
  }

  protected void visitCollectionWith(char[] ch, Iterator<? extends JNode> iter) {
    if (iter.hasNext()) {
      accept(iter.next());
    }
    while (iter.hasNext()) {
      print(ch);
      accept(iter.next());
    }
  }
}
