/*
 * Copyright 2013 Guidewire Software, Inc.
 */

package gw.plugin.ij.util.transform.java.Visitor;

import com.sun.source.tree.*;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.util.Convert;

import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import java.util.*;
import java.util.regex.Pattern;


public class GosuVisitor implements TreeVisitor<String, Void> {
  StringBuilder output;
  private int ident = 0;
  private static int TAB_SIZE ;
  private static String TAB;
  private SymbolTable symTable;
  private String currentEnumIdent;
  private List<String> currentResourcesIdents;
  private Mode mode;
  private boolean isEnum;
  private boolean isInterface;
  boolean skipBlockScope;
  private boolean skipSymConversion;

  private enum Mode {NORMAL, USING_NO_MODIFIERS, CATCH_PARAM, USING_CAST, METHOD_PARAM, ADD_RESOURCES_FINALLY_BLOCK, CLASS_VAR}

  public StringBuilder getOutput() {
    return output;
  }

  public GosuVisitor(int tabSize) {
    TAB_SIZE = tabSize;
    TAB = genTabSpaces(TAB_SIZE);
    mode = Mode.NORMAL;
    symTable = new SymbolTable();
    output = new StringBuilder();
    skipBlockScope = false;
    skipSymConversion = false;
  }

  private String genTabSpaces(int x) {
    String tab = "";
    while(x > 0) {
      tab += " ";
      x--;
    }
    return tab;
  }

  public String visitImport(ImportTree node, Void v) {
    StringBuilder out = new StringBuilder();
    Tree qualId = node.getQualifiedIdentifier();
    out.append("uses ");
    out.append(qualId.accept(this, v));
    out.append("\n");
    return out.toString();
  }

  public String visitCompilationUnit(CompilationUnitTree node, Void v) {
    ident = 0;

    if (!node.getPackageAnnotations().isEmpty()) {
      throw new AssertionError("Annotations on packages not supported");
    }
    ExpressionTree packageName = node.getPackageName();
    if (packageName != null) {
      output.append("package ");
      output.append(packageName);
      output.append("\n\n");
    }
    output.append("uses java.lang.*\n");
    List<? extends ImportTree> imports = node.getImports();
    for (ImportTree imp : imports) {
      output.append(imp.accept(this, v));
    }
    output.append("\n");
    List<? extends Tree> typeDecls = node.getTypeDecls();
    for (Tree typeDecl : typeDecls) {
      output.append(typeDecl.accept(this, v));
    }
    return output.toString();
  }


  public String visitClass(ClassTree node, Void v) {
    StringBuilder out = new StringBuilder();

    mode = Mode.NORMAL;
    Name name = node.getSimpleName();
    List<? extends TypeParameterTree> typeParameters = node.getTypeParameters();
    String declType = null;
    Tree.Kind kind = node.getKind();
    isEnum = false;
    isInterface = false;
    switch (kind) {
      case CLASS:
        declType = "class";
        break;
      case ENUM:
        declType = "enum";
        isEnum = true;
        currentEnumIdent = name.toString();
        break;
      case INTERFACE:
        declType = "interface";
        isInterface = true;
        break;
      case ANNOTATION_TYPE:
        appendAsComment(out, node.toString());
        //throw new AssertionError("Annotation declaration not supported in the conversion");
        return out.toString();
        //break;
      default:
        declType = "???";
        break;
    }
    ModifiersTree modifiers = node.getModifiers();
    out.append(modifiers.accept(this, v));
    out.append(declType);
    out.append(" ");
    out.append(name);
    if(!typeParameters.isEmpty()) {
      out.append("<");
      boolean first = true;
      for(Tree t : typeParameters) {
        if(!first) {
          out.append(", ");
        }
        first = false;
        out.append(t.accept(this, v));
      }
      out.append(">");
    }
    out.append(" ");
    Tree extendsClause = node.getExtendsClause();
    if (extendsClause != null) {
      out.append("extends ");
      out.append(extendsClause.accept(this, v));
      out.append(" ");
    }
    List<? extends Tree> implementsClause = node.getImplementsClause();
    if (!implementsClause.isEmpty()) {
      if (isInterface) {
        out.append("extends ");
      } else {
        out.append("implements ");
      }
      boolean first = true;
      for (Tree implement : implementsClause) {
        if (first) {
          out.append(implement.accept(this, v));
          first = false;
        } else {
          out.append(", ");
          out.append(implement.accept(this, v));
        }
      }
    }
    out.append(" {\n");
    pushIndent();
    symTable.pushGlobalScope(name.toString());
    List<? extends Tree> members = node.getMembers();

    addGlobalVariables(members);

    boolean first = true;
    boolean isLastVar = false;
    for (Tree member : members) {
      isLastVar = false;
      if (isEnum && isAEnumConstant(member)) {
        if (!first) {
          out.append(",\n");
        }
        first = false;
        appendIndent(out);
        String enumConst = member.accept(this, v);
        out.append(enumConst);
      } else {
        out.append("\n");
        appendIndent(out);
        if(member instanceof VariableTree) {
          Mode old = mode;
          mode = Mode.CLASS_VAR;
          out.append(member.accept(this, v));
          mode = old;
          isLastVar = true;
        } else if (member instanceof BlockTree){
          appendAsComment(out, member.toString());
        } else {
          out.append(member.accept(this, v));
        }
      }

    }

    if(isLastVar) {
      out.append("\n");
    }
    if (isEnum) {
      out.append("\n");
      isEnum = false;
    }
    if (isInterface) {
      isInterface = false;
    }
    mode = Mode.NORMAL;
    symTable.popGlobalScope();
    popIndent();
    out.append("\n");
    appendIndent(out);
    out.append("}\n");
    return out.toString();
  }

  private void addGlobalVariables(List<? extends Tree> members) {
    for (Tree member : members) {
      if (member instanceof VariableTree) {
        String ident = ((VariableTree) member).getName().toString();
        symTable.addGlobally(ident);
      }
    }
  }

  public String visitReturn(ReturnTree node, Void v) {
    StringBuilder out = new StringBuilder();
    ExpressionTree expression = node.getExpression();

    out.append("return");
    if(expression != null) {
      out.append(" ");
      out.append(expression.accept(this, v));
    }
    return out.toString();
  }

  public String visitTry(TryTree node, Void v) {
    StringBuilder out = new StringBuilder();
    List<? extends Tree> resources = node.getResources();
    BlockTree block = node.getBlock();
    List<? extends CatchTree> catches = node.getCatches();
    BlockTree finallyBlock = node.getFinallyBlock();
    List<String> resIdents = new ArrayList<String>();

    if(catches.isEmpty() && !resources.isEmpty()) {
      out.append("using (");
      boolean first = true;
      Mode oldMode = mode;
      mode = Mode.USING_NO_MODIFIERS;
      for(Tree res : resources) {
        if (!first) {
           out.append(", ");
         }
        first = false;
        out.append(res.accept(this, v));
      }
      mode = oldMode;
      out.append(")");
      out.append(block.accept(this, v));
      if(finallyBlock != null) {
        appendIndent(out);
        out.append("finally ");
        out.append(finallyBlock.accept(this, v));
      }
      return out.toString();
    }
    if(!resources.isEmpty()) {
      for(Tree res : resources) {
        out.append("\n");
        appendIndent(out);
        out.append(res.accept(this, v));
        String ident = ((JCTree.JCVariableDecl) res).getName().toString();
        resIdents.add(ident);
      }
    }
    out.append("\n");
    appendIndent(out);
    out.append("try");
    out.append(block.accept(this, v));
    if(!catches.isEmpty()) {
      for(CatchTree c : catches) {
        out.append(c.accept(this, v));
      }
    }
    if(finallyBlock != null || !resIdents.isEmpty()) {
      Mode oldMode = mode;
      mode = Mode.ADD_RESOURCES_FINALLY_BLOCK;
      currentResourcesIdents = resIdents;
      appendIndent(out);
      out.append("finally ");
      if(finallyBlock == null) {
        finallyBlock  = new DummyBlock();
      }
      out.append(finallyBlock.accept(this, v));
      mode = oldMode;
    }
    return out.toString();
  }


  public String visitCatch(CatchTree node, Void v) {
    StringBuilder out = new StringBuilder();
    VariableTree parameter = node.getParameter();
    BlockTree block = node.getBlock();

    Tree type = parameter.getType();
    if(type instanceof JCTree.JCTypeUnion) {
      String name = parameter.getName().toString();
      for(JCTree.JCExpression expr : ((JCTree.JCTypeUnion) type).getTypeAlternatives()) {
        appendIndent(out);
        out.append("catch (");
        skipBlockScope = true;
        symTable.pushLocalScope();
        String newName = symTable.addLocally(name);
        out.append(newName).append(" : ");
        out.append(expr.accept(this, v));
        out.append(")");
        out.append(block.accept(this, v));
        symTable.popLocalScope();
      }
    } else {
      appendIndent(out);
      out.append("catch (");
      skipBlockScope = true;
      Mode oldMode = mode;
      mode = Mode.CATCH_PARAM;
      symTable.pushLocalScope();
      out.append(parameter.accept(this, v));
      mode = oldMode;
      out.append(")");
      out.append(block.accept(this, v));
      symTable.popLocalScope();
    }
    return out.toString();
  }


  public String visitLabeledStatement(LabeledStatementTree node, Void v) {
    StringBuilder out = new StringBuilder();
    StatementTree stmt = node.getStatement();
    String label = node.getLabel().toString() + ": ";
    appendAsComment(out, label);
    appendIndent(out);
    out.append(stmt.accept(this, v));
    return out.toString();
  }


  public String visitBlock(BlockTree node, Void v) {
    StringBuilder out = new StringBuilder();
    // todo handle static blocks
    if (node.isStatic()) {
      //throw new AssertionError("Static blocks not supported in Gosu");
      appendAsComment(out, node.toString());
      return out.toString();
    }
    out.append(" {\n");
    pushIndent();
    boolean skipPopScope = false;
    if (!skipBlockScope) {
      symTable.pushLocalScope();
    }  else {
      skipBlockScope = false;
      skipPopScope = true;
    }
    if(mode == Mode.ADD_RESOURCES_FINALLY_BLOCK) {
      int i = currentResourcesIdents.size()-1;
      while(i >= 0) {
        String id =  currentResourcesIdents.get(i);
        appendIndent(out);
        String newId = symTable.convertLocalSymbol(id);
        out.append("if (").append(newId).append(" != null) ").append(newId).append(".close()\n");
        i--;
      }
    }
    List<? extends StatementTree> statements = node.getStatements();
    for (StatementTree stm : statements) {
      appendIndent(out);
      out.append(stm.accept(this, v));
      out.append("\n");
    }

    if (!skipPopScope) {
      symTable.popLocalScope();
    }
    popIndent();
    appendIndent(out);
    out.append("}\n");

    return out.toString();
  }

  public String visitSwitch(SwitchTree node, Void v) {
    StringBuilder out = new StringBuilder();
    List<? extends CaseTree> cases = node.getCases();
    ExpressionTree expression = node.getExpression();
    out.append("switch ");
    out.append(expression.accept(this, v));
    out.append(" {\n");
    pushIndent();
    symTable.pushLocalScope();
    for(CaseTree c : cases) {
      out.append(c.accept(this, v));
      out.append("\n");
    }
    symTable.popLocalScope();
    popIndent();
    appendIndent(out);
    out.append("}\n");
    return out.toString();
  }


  public String visitCase(CaseTree node, Void v) {
    StringBuilder out = new StringBuilder();
    ExpressionTree expr = node.getExpression();
    List<? extends StatementTree> statements = node.getStatements();
    appendIndent(out);
    if(expr != null) {
      out.append("case ");
      out.append(expr.accept(this, v));
    } else {
      out.append("default");
    }
    out.append(":\n");
    pushIndent();
    for(StatementTree stm : statements) {
      appendIndent(out);
      out.append(stm.accept(this, v));
      out.append("\n");
    }
    popIndent();
    appendIndent(out);
    return out.toString();
  }


  public String visitEnhancedForLoop(EnhancedForLoopTree node, Void v) {
    StringBuilder out = new StringBuilder();
    String variablename = node.getVariable().getName().toString();
    ExpressionTree expr = node.getExpression();
    StatementTree stmt = node.getStatement();

    skipBlockScope = true;
    symTable.pushLocalScope();
    out.append("for (");
    String newName = symTable.addLocally(variablename);
    out.append(newName);
    out.append(" in ");
    out.append(expr.accept(this, v));
    out.append(")");
    if(stmt instanceof BlockTree) {
      out.append(stmt.accept(this, v));
    } else {
      out.append(" {\n");
      pushIndent();
      appendIndent(out);
      out.append(stmt.accept(this, v));
      popIndent();
      out.append("\n");
      appendIndent(out);
      out.append("}\n");
    }
    symTable.popLocalScope();
    return out.toString();
  }


  public String visitEmptyStatement(EmptyStatementTree node, Void v) {
    return "";
  }


  public String visitExpressionStatement(ExpressionStatementTree node, Void v) {
    StringBuilder out = new StringBuilder();
    out.append(node.getExpression().accept(this, v));
    return out.toString();
  }


  public String visitArrayAccess(ArrayAccessTree node, Void v) {
    StringBuilder out = new StringBuilder();
    ExpressionTree expr = node.getExpression();
    ExpressionTree index = node.getIndex();

    out.append(expr.accept(this, v));
    out.append("[");
    out.append(index.accept(this, v));
    out.append("]");
    return out.toString();
  }


  public String visitArrayType(ArrayTypeTree node, Void v) {
    return node.getType().accept(this, v) + "[]";
  }


  public String visitThrow(ThrowTree node, Void v) {
    return "throw " + node.getExpression().accept(this, v);
  }


  public String visitVariable(VariableTree node, Void v) {
    StringBuilder out = new StringBuilder();
    String name = node.getName().toString();
    ExpressionTree initializer = node.getInitializer();
    if (mode != Mode.CLASS_VAR && !isAEnumConstant(node)) {
      name = symTable.addLocally(name);
    }
    if (isEnum && isAEnumConstant(node)) {
      out.append(name);
      if (initializer != null) {
        out.append(initializer.accept(this, v));
      }
      return out.toString();
    }
    if (mode != Mode.CATCH_PARAM && mode != Mode.USING_NO_MODIFIERS) {
      ModifiersTree modifiers = node.getModifiers();
      out.append(modifiers.accept(this, v));
    }
    Tree type = node.getType();
    boolean appedVar = !(mode == Mode.METHOD_PARAM || mode == Mode.CATCH_PARAM);
    if (appedVar) {
      out.append("var ");
    }
    out.append(name);
    skipSymConversion = true;
    String varType = type.accept(this, v);
    skipSymConversion = false;
    String iniz = null;
    String genType = null;
    if (initializer != null) {
      iniz = " = " + initializer.accept(this, v);
      if (iniz.contains("<>")) {
        genType = extractGenericType(varType);
        iniz = iniz.replaceAll("<>", genType);
      }
    }
    if (appedVar && initializer != null && mode != Mode.CLASS_VAR) {
      String infer = typeInference(initializer, varType, genType, iniz);
      if(infer != null) {
        out.append(infer);
        return out.toString();
      }
    }
    out.append(" : ");
    out.append(varType);
    if (iniz != null) {
      out.append(iniz);
    }
    return out.toString();
  }

  private String extractGenericType(String varType) {
    String out = varType;
    int b = varType.indexOf("<");
    int e = varType.lastIndexOf(">");
    if(b != -1 && e != -1) {
      out = varType.substring(b, e + 1);
    }
    return out;
  }

  private String typeInference(ExpressionTree initializer, String varType, String genType, String iniz) {
    String initType = null;
    boolean literal = false;
    if (initializer instanceof NewArrayTree) {
      Tree arrayType = ((NewArrayTree) initializer).getType();
      if(arrayType != null) {
        initType = arrayType.accept(this, null);
        initType = initType.replaceAll(Pattern.quote("[]"), "");
        varType = varType.replaceAll(Pattern.quote("[]"), "");
      }
    } else if (initializer instanceof NewClassTree) {
      initType = ((NewClassTree) initializer).getIdentifier().accept(this, null);
      if (genType != null) {
        initType = initType.replaceAll("<>", genType);
      }
    } else if(initializer instanceof LiteralTree &&
            initializer.getKind() != Tree.Kind.NULL_LITERAL &&
            !varType.equals("Object"))
    {
      literal = true;
      if(varType.equals("byte")) {
        iniz = iniz + "b";
      } else if(varType.equals("short")) {
        iniz = iniz + "s";
      } else if(varType.equals("float") && initializer.getKind() == Tree.Kind.INT_LITERAL ) {
        iniz = iniz + "f";
      } else if(varType.equals("double") && initializer.getKind() == Tree.Kind.INT_LITERAL ) {
        iniz = iniz + ".0";
      } else if(varType.equals("long") && initializer.getKind() == Tree.Kind.INT_LITERAL ) {
        iniz = iniz + "L";
      }
    }
    if((initType != null && varType.equals(initType)) || literal) {
      return iniz;
    }
    return null;
  }

  private boolean isAEnumConstant(Tree node) {
    return ((JCTree)node).getTag() == JCTree.VARDEF && (((JCTree.JCVariableDecl) node).mods.flags & Flags.ENUM) != 0;
  }


  public String visitParameterizedType(ParameterizedTypeTree node, Void v) {
    StringBuilder out = new StringBuilder();
    Tree type = node.getType();
    List<? extends Tree> typeArguments = node.getTypeArguments();
    out.append(type.accept(this, v));
    out.append("<");
    boolean first = true;
    for(Tree t : typeArguments) {
      if(!first) {
        out.append(", ");
      }
      first = false;
      out.append(t.accept(this, v));
    }
    out.append(">");
    return out.toString();
  }


  public String visitMemberSelect(MemberSelectTree node, Void v) {
    StringBuilder out = new StringBuilder();

    String expr = node.getExpression().accept(this, v);
    String id = node.getIdentifier().toString();

    if (expr.endsWith(".this")) {
      int e = expr.indexOf(".this");
      int dot = expr.substring(0, e).lastIndexOf(".");
      int s = dot != -1 ? dot + 1 : 0;

      String clazz = expr.substring(s, e);
      int level = symTable.getClassLevelFromCurrent(clazz);
      if (level == 0) {
        return id;
      } else {
        out.append("outer");
        level--;
        while (level > 0) {
          out.append(".outer");
          level--;
        }
      }
    } else {
      out.append(expr);
    }

    if (!"class".equals(id)) {
      out.append(".");
      out.append(id);
    }
    return out.toString();
  }


  public String visitWildcard(WildcardTree node, Void v) {
    String out;
    Tree bound = node.getBound();
    String boundStr = "";
    if(bound != null) {
      boundStr = bound.accept(this, v);
    }
    Tree.Kind kind = node.getKind();
    if(kind == Tree.Kind.EXTENDS_WILDCARD) {
      out = boundStr;
    } else if(kind == Tree.Kind.SUPER_WILDCARD) {
      out = "? super " + boundStr;
    } else {//if(kind == Tree.Kind.UNBOUNDED_WILDCARD) {
      out = "?";
    }
    return out;
  }


  public String visitBinary(BinaryTree node, Void v) {
    StringBuilder out = new StringBuilder();
    ExpressionTree lOp = node.getLeftOperand();
    ExpressionTree rOp = node.getRightOperand();
    int operator = ((JCTree.JCBinary) node).getTag();
    out.append(lOp.accept(this, v));
    out.append(" ").append(operatorName(operator)).append(" ");
    out.append(rOp.accept(this, v));
    return out.toString();
  }


  public String visitParenthesized(ParenthesizedTree node, Void v) {
    StringBuilder out = new StringBuilder();

    out.append("(");
    out.append(node.getExpression().accept(this, v));
    if(mode == Mode.USING_CAST) {
      out.append(" as IMonitorLock");
    }
    out.append(")");
    return out.toString();
  }


  public String visitNewArray(NewArrayTree node, Void v) {
    StringBuilder out = new StringBuilder();
    List<? extends ExpressionTree> dim = node.getDimensions();
    List<? extends ExpressionTree> inits = node.getInitializers();
    Tree type = node.getType();
    if (type != null) {
      out.append("new ");
      skipSymConversion = true;
      out.append(type.accept(this, v));
      skipSymConversion = false;
    }
    if (!dim.isEmpty()) {
      if(type != null && type instanceof JCTree.JCArrayTypeTree) {
        int end = out.length();
        out.delete(end - 2, end);
      }
      for (ExpressionTree d : dim) {
        out.append("[");
        out.append(d.accept(this, v));
        out.append("]");
      }
      if(type != null && type instanceof JCTree.JCArrayTypeTree) {
        out.append("[]");
      }
    } else if (type != null) {
      out.append("[]");
    }
    if (inits != null) {
      boolean first = true;
      out.append("{");
      for (ExpressionTree init : inits) {
        if (!first) {
          out.append(", ");
        }
        first = false;
        out.append(init.accept(this, v));
      }
      out.append("}");
    }
    return out.toString();
  }


  public String visitOther(Tree node, Void v) {
    return "OTHER";
  }


  public String visitErroneous(ErroneousTree node, Void v) {
    throw new AssertionError("We don't support erroneous Java files");
  }


  public String visitSynchronized(SynchronizedTree node, Void v) {
    StringBuilder out = new StringBuilder();
    ExpressionTree expr = node.getExpression();
    BlockTree block = node.getBlock();
    out.append("using ");
    Mode oldMode = mode;
    mode = Mode.USING_CAST;
    out.append(expr.accept(this, v));
    mode = oldMode;
    out.append(block.accept(this, v));
    return out.toString();
  }


  public String visitBreak(BreakTree node, Void v) {
    StringBuilder out = new StringBuilder();
    out.append("break");
    Name label = node.getLabel();
    if(label != null) {
       out.append(" ").append(label);
    }
    return out.toString();
  }


  public String visitContinue(ContinueTree node, Void v) {
    StringBuilder out = new StringBuilder();
    out.append("continue");
    Name label = node.getLabel();
    if(label != null) {
      out.append(" ").append(label);
    }
    return out.toString();
  }


  public String visitDoWhileLoop(DoWhileLoopTree node, Void v) {
    StringBuilder out = new StringBuilder();
    ExpressionTree condition = node.getCondition();
    StatementTree statement = node.getStatement();
    out.append("do");
    if(statement instanceof BlockTree) {
      out.append(statement.accept(this, v));
    } else {
      out.append(" {\n");
      pushIndent();
      symTable.pushLocalScope();
      appendIndent(out);
      out.append(statement.accept(this, v));
      symTable.popLocalScope();
      popIndent();
      out.append("\n");
      appendIndent(out);
      out.append("}\n");
    }
    appendIndent(out);
    out.append("while ");
    out.append(condition.accept(this, v));
    return out.toString();
  }


  public String visitWhileLoop(WhileLoopTree node, Void v) {
    StringBuilder out = new StringBuilder();
    ExpressionTree condition = node.getCondition();
    StatementTree statement = node.getStatement();
    out.append("while ");
    out.append(condition.accept(this, v));
    if(statement instanceof BlockTree) {
      out.append(statement.accept(this, v));
    } else {
      out.append(" {\n");
      pushIndent();
      symTable.pushLocalScope();
      appendIndent(out);
      out.append(statement.accept(this, v));
      symTable.popLocalScope();
      popIndent();
      out.append("\n");
      appendIndent(out);
      out.append("}\n");
    }
    return out.toString();
  }


  public String visitIf(IfTree node, Void v) {
    StringBuilder out = new StringBuilder();
    ExpressionTree cond = node.getCondition();
    StatementTree thenStm = node.getThenStatement();
    StatementTree elseStm = node.getElseStatement();
    out.append("if ");
    out.append(cond.accept(this, v));
    if(thenStm instanceof BlockTree) {
      out.append(thenStm.accept(this, v));
      out.deleteCharAt(out.length()-1);
    } else {
      out.append(" {\n");
      pushIndent();
      symTable.pushLocalScope();
      appendIndent(out);
      out.append(thenStm.accept(this, v));
      symTable.popLocalScope();
      popIndent();
      out.append("\n");
      appendIndent(out);
      out.append("}");
    }
    if(elseStm != null) {
      out.append(" else");
      if(elseStm instanceof BlockTree || elseStm instanceof IfTree) {
        if(elseStm instanceof IfTree) {
          out.append(" ");
        }
        out.append(elseStm.accept(this, v));
        if(elseStm instanceof BlockTree) {
          out.deleteCharAt(out.length()-1);
        }
      } else {
        out.append(" {\n");
        pushIndent();
        symTable.pushLocalScope();
        appendIndent(out);
        out.append(elseStm.accept(this, v));
        symTable.popLocalScope();
        popIndent();
        out.append("\n");
        appendIndent(out);
        out.append("}");
      }
    }
    return out.toString();
  }


  public String visitInstanceOf(InstanceOfTree node, Void v) {
    StringBuilder out = new StringBuilder();
    ExpressionTree expr = node.getExpression();
    Tree type = node.getType();
    out.append(expr.accept(this, v));
    out.append(" typeis ");
    out.append(type.accept(this, v));
    return out.toString();
  }


  public String visitUnary(UnaryTree node, Void v) {
    int op = ((JCTree.JCUnary) node).getTag();
    String opName = operatorName(op);
    String expr = node.getExpression().accept(this, v);
    if(op == JCTree.PREDEC  ||
       op == JCTree.PREINC  ||
       op == JCTree.POSTDEC ||
       op == JCTree.POSTINC) {
      return expr + opName;
    }
    return opName + expr;
  }


  public String visitAssert(AssertTree node, Void v) {
    StringBuilder out = new StringBuilder();
    ExpressionTree condition = node.getCondition();
    ExpressionTree detail = node.getDetail();
    out.append("assert ");
    out.append(condition.accept(this, v));
    if (detail != null) {
      out.append(" : ");
      out.append(detail.accept(this, v));
    }
    return out.toString();
  }


  public String visitForLoop(ForLoopTree node, Void v) {
    StringBuilder out = new StringBuilder();
    List<? extends StatementTree> initializer = node.getInitializer();
    ExpressionTree condition = node.getCondition();
    StatementTree statement = node.getStatement();
    List<? extends ExpressionStatementTree> update = node.getUpdate();

    String gosuForLoop = maybeTranfromToGosuFor(node, v);
    if(gosuForLoop != null) {
      return gosuForLoop;
    }

    if(!initializer.isEmpty()) {
      for(StatementTree stm : initializer) {
        out.append(stm.accept(this, v));
        out.append("\n");
        appendIndent(out);
      }
    }
    out.append("while (");
    if(condition == null) {
      out.append("true");
    } else {
      out.append(condition.accept(this, v));
    }

    out.append(") {\n");
    pushIndent();
    symTable.pushLocalScope();
    if(statement instanceof BlockTree) {
      List<? extends StatementTree> statements = ((BlockTree) statement).getStatements();
      for(StatementTree stm : statements) {
        appendIndent(out);
        out.append(stm.accept(this, v));
        out.append("\n");
      }
    } else {
      appendIndent(out);
      out.append(statement.accept(this, v));
      out.append("\n");
    }
    if(!update.isEmpty()) {
      for(ExpressionStatementTree stm : update) {
        appendIndent(out);
        out.append(stm.accept(this, v));
        out.append("\n");
      }
    }
    symTable.popLocalScope();
    popIndent();
    appendIndent(out);
    out.append("}\n");
    return out.toString();
  }

  private String maybeTranfromToGosuFor(ForLoopTree node, Void v) {
    StringBuilder out = new StringBuilder();
    List<? extends StatementTree> initializer = node.getInitializer();
    ExpressionTree condition = node.getCondition();
    StatementTree statement = node.getStatement();
    List<? extends ExpressionStatementTree> update = node.getUpdate();

    if (initializer.size() == 1 && update.size() == 1 && condition instanceof JCTree.JCBinary) {
      StatementTree inizST = initializer.get(0);
      ExpressionTree expression = update.get(0).getExpression();
      JCTree.JCBinary cond = (JCTree.JCBinary) condition;
      if (cond.getTag() == JCTree.LT && inizST instanceof JCTree.JCVariableDecl &&
          expression instanceof JCTree.JCUnary)
      {
        JCTree.JCVariableDecl iniz = (JCTree.JCVariableDecl) inizST;
        JCTree.JCUnary up = (JCTree.JCUnary) expression;
        String var = cond.getLeftOperand().accept(this, v);
        if (var.equals(up.getExpression().accept(this, v)) &&
            var.equals(iniz.getName().toString()) &&
            (up.getTag() == JCTree.PREINC || up.getTag() == JCTree.POSTINC) &&
            "0".equals(iniz.getInitializer().accept(this, v)))
        {
          JCTree.JCExpression condRight = cond.getRightOperand();
          boolean validLimit = false;
          String condR = condRight.accept(this, v);
          if (condRight instanceof JCTree.JCLiteral) {
            try {
              Integer val = Integer.valueOf(condR);
              if (val >= 0) {
                validLimit = true;
              }
            } catch (NumberFormatException ex) {}
          } else if (condR.endsWith(".size()") || condR.endsWith(".length()") || condR.endsWith(".length")) {
            validLimit = true;
          }
          if(validLimit) {
            skipBlockScope = true;
            symTable.pushLocalScope();
            String newName = symTable.addLocally(var);
            out.append("for (");
            out.append(newName);
            out.append(" in 0..|");
            out.append(condR);
            out.append(")");
            if(statement instanceof BlockTree) {
              out.append(statement.accept(this, v));
            } else {
              out.append(" {\n");
              pushIndent();
              appendIndent(out);
              out.append(statement.accept(this, v));
              popIndent();
              out.append("\n");
              appendIndent(out);
              out.append("}\n");
            }
            symTable.popLocalScope();
            return out.toString();
          }
        }
      }
    }
    return null;
  }


  public String visitConditionalExpression(ConditionalExpressionTree node, Void v) {
    StringBuilder out = new StringBuilder();

    ExpressionTree cond = node.getCondition();
    ExpressionTree falseExp = node.getFalseExpression();
    ExpressionTree trueEx = node.getTrueExpression();
    out.append(cond.accept(this, v));
    out.append(" ? ");
    out.append(trueEx.accept(this, v));
    out.append(" : ");
    out.append(falseExp.accept(this, v));
    return out.toString();
  }


  public String visitMethodInvocation(MethodInvocationTree node, Void v) {
    StringBuilder out = new StringBuilder();
    List<? extends ExpressionTree> arguments = node.getArguments();
    ExpressionTree methodSelect = node.getMethodSelect();
    if(methodSelect instanceof IdentifierTree) {
      skipSymConversion = true;
      out.append(methodSelect.accept(this, v));
      skipSymConversion = false;
    } else {
      out.append(methodSelect.accept(this, v));
    }
    List<? extends Tree> typeArguments = node.getTypeArguments();
    if (!typeArguments.isEmpty()) {
      out.append("<");
      boolean first = true;
      for(Tree t : typeArguments) {
        if(!first) {
          out.append(", ");
        }
        first = false;
        out.append(t.accept(this, v));
      }
      out.append(">");
    }
    out.append("(");
    if (!arguments.isEmpty()) {
      boolean first = true;
      for (ExpressionTree arg : arguments) {
        if (!first) {
          out.append(", ");
        }
        first = false;
        out.append(arg.accept(this, v));
      }
    }
    out.append(")");
    return out.toString();
  }


  public String visitNewClass(NewClassTree node, Void v) {
    StringBuilder out = new StringBuilder();
    ExpressionTree identifier = node.getIdentifier();
    List<? extends ExpressionTree> arguments = node.getArguments();
    ClassTree classBody = node.getClassBody();
    ExpressionTree enclosingExpression = node.getEnclosingExpression();

    //List<? extends Tree> typeArguments = node.getTypeArguments();
    //not needed in gosu

    if (enclosingExpression != null) {
      out.append(enclosingExpression.accept(this, v));
      out.append(".");
      //throw new AssertionError("Annotation declaration not supported in the conversion");
    }

    skipSymConversion = true;
    String identOut = identifier.accept(this, v);
    skipSymConversion = false;
    boolean isEnumConst = isEnum && currentEnumIdent.equals(identOut);
    if (!isEnumConst) {
      out.append("new ");
      out.append(identOut);
    }
    if (!isEnumConst || !arguments.isEmpty()) {
      out.append("(");
    }
    if (!arguments.isEmpty()) {
      boolean first = true;
      for (ExpressionTree arg : arguments) {
        if (!first) {
          out.append(", ");
        }
        first = false;
        out.append(arg.accept(this, v));
      }
    }
    if (!isEnumConst || !arguments.isEmpty()) {
      out.append(")");
    }
    if (classBody != null) {
      out.append(" {\n");
      pushIndent();
      symTable.pushGlobalScope(identOut);
      List<? extends Tree> members = classBody.getMembers();

      addGlobalVariables(members);

      for (Tree member : members) {
        out.append("\n");
        appendIndent(out);
        if(member instanceof VariableTree) {
          Mode old = mode;
          mode = Mode.CLASS_VAR;
          out.append(member.accept(this, v));
          mode = old;
        } else if (member instanceof BlockTree){
          appendAsComment(out, member.toString());
        } else {
          out.append(member.accept(this, v));
        }
      }
      symTable.popGlobalScope();
      popIndent();
      out.append("\n");
      appendIndent(out);
      out.append("}\n");
    }
    return out.toString();
  }


  public String visitPrimitiveType(PrimitiveTypeTree node, Void v) {
    return node.toString();
  }


  public String visitMethod(MethodTree node, Void v) {
    StringBuilder out = new StringBuilder();
    BlockTree body = node.getBody();
    Tree defaultValue = node.getDefaultValue();
    if (defaultValue != null) {
      throw new AssertionError("default values for annotations not supported");
    }
    ModifiersTree modifiers = node.getModifiers();
    String name = node.getName().toString();
    List<? extends VariableTree> parameters = node.getParameters();
    Tree returnType = node.getReturnType();
    List<? extends TypeParameterTree> typeParameters = node.getTypeParameters();
    //List<? extends ExpressionTree> lThrows = node.getThrows(); not needed
    HashMap<String, String> constructorTypes = null;


    out.append(modifiers.accept(this, v));
    if (name.equals("<init>")) {
      if(isEnum && !modifiers.getFlags().contains(Modifier.PRIVATE) ) {
        out.append("private ");
      }
      out.append("construct");
      if(!typeParameters.isEmpty()) {
        constructorTypes = computeConstructorTypes(typeParameters);
      }
    } else {
      out.append("function ");
      out.append(name);
      if(!typeParameters.isEmpty()) {
        out.append("<");
        boolean first = true;
        for(Tree t : typeParameters) {
          if(!first) {
            out.append(", ");
          }
          first = false;
          out.append(t.accept(this, v));
        }
        out.append(">");
      }
    }
    Mode oldMode = mode;
    mode = Mode.METHOD_PARAM;
    out.append("(");
    symTable.pushLocalScope();
    boolean first = true;
    for (VariableTree par : parameters) {
      if (!first) {
        out.append(", ");
      }
      first = false;
      out.append(par.accept(this, v));
    }
    out.append(")");
    if(constructorTypes != null) {
      String newOut = out.toString();
      for(String key : constructorTypes.keySet()) {
        newOut = newOut.replace(": " + key, ": " + constructorTypes.get(key));
      }
      out = new StringBuilder(newOut);
    }
    mode = oldMode;
    if (returnType != null) {
      out.append(" : ");
      out.append(returnType.accept(this, v));
    }
    if (body != null) {
      skipBlockScope = true;
      out.append(body.accept(this, v));
    }
    symTable.popLocalScope();
    return out.toString();
  }


  private HashMap<String, String> computeConstructorTypes(List<? extends TypeParameterTree> typeParameters) {
    HashMap<String, String> constructorTypes = new HashMap<String, String>();
    for(TypeParameterTree t : typeParameters) {
      String key = t.getName().toString();
      String value;
      List<? extends Tree> bounds = t.getBounds();
      if(bounds.isEmpty()) {
        value = "Object";
      } else {
        StringBuilder out = new StringBuilder();
        if(!bounds.isEmpty()) {
          boolean first = true;
          for(Tree b : bounds) {
            if(!first) {
              out.append(" & ");
            }
            first = false;
            out.append(b.accept(this, null));
          }
        }
        value = out.toString();
      }
      constructorTypes.put(key, value);
    }
    return constructorTypes;
  }


  public String visitModifiers(ModifiersTree node, Void v) {
    StringBuilder out = new StringBuilder();
    String ann;
    boolean isOverridden = false;
    for (AnnotationTree annotation : node.getAnnotations()) {
      ann = annotation.accept(this, v);
      if("override".equals(ann)) {
        isOverridden = true;
      } else {
        out.append(ann);
        out.append("\n");
      }
    }
    if (!node.getAnnotations().isEmpty() && !isOverridden) {
      appendIndent(out);
    }
    if(isOverridden) {
      if(node.getAnnotations().size() > 1) {
        appendIndent(out);
      }
      out.append("override ");
    }
    for (Modifier modifier : node.getFlags()) {
        out.append(modifier);
        out.append(" ");
    }
    String modifiers = out.toString();
    if(mode == Mode.CLASS_VAR &&
       !isInterface &&
       !modifiers.contains("public") &&
       !modifiers.contains("protected") &&
       !modifiers.contains("private"))
    {
      out.append("internal ");
    }
    return out.toString();
  }


  public String visitTypeParameter(TypeParameterTree node, Void v) {
    StringBuilder out = new StringBuilder();
    Name name = node.getName();
    List<? extends Tree> bounds = node.getBounds();
    out.append(name);
    if(!bounds.isEmpty()) {
      out.append(" extends ");
      boolean first = true;
      for(Tree b : bounds) {
        if(!first) {
          out.append(" & ");
        }
        first = false;
        out.append(b.accept(this, v));
      }
    }
    return out.toString();
  }


  public String visitIdentifier(IdentifierTree node, Void v) {
    String ident = node.getName().toString();
    if(!skipSymConversion) {
      ident = symTable.convertLocalSymbol(ident);
    }
    return ident;
  }


  public String visitLiteral(LiteralTree node, Void v) {
    StringBuilder out = new StringBuilder();
    Object value = node.getValue();
    switch (node.getKind()) {
      case INT_LITERAL:
        out.append(value);
        break;
      case LONG_LITERAL:
        out.append(value);
        out.append("L");
        break;
      case FLOAT_LITERAL:
        out.append(value);
        out.append("f");
        break;
      case DOUBLE_LITERAL:
        out.append(value);
        break;
      case BOOLEAN_LITERAL:
      case NULL_LITERAL:
        out.append(value);
        break;
      case CHAR_LITERAL:
        out.append("\'").append(Convert.quote(String.valueOf(value))).append("\'");
        break;
      case STRING_LITERAL:
        out.append("\"").append(Convert.quote(value.toString())).append("\"");
        break;
    }
    return out.toString();
  }


  public String visitTypeCast(TypeCastTree node, Void v) {
    StringBuilder out = new StringBuilder();
    out.append(node.getExpression().accept(this, v));
    out.append(" as ");
    out.append(node.getType().accept(this, v));
    return out.toString();
  }


  public String visitAssignment(AssignmentTree node, Void v) {
    StringBuilder out = new StringBuilder();
    ExpressionTree variable = node.getVariable();
    ExpressionTree expression = node.getExpression();
    out.append(variable.accept(this, v));
    out.append(" = ");
    out.append(expression.accept(this, v));
    return out.toString();
  }


  public String visitCompoundAssignment(CompoundAssignmentTree node, Void v) {
    StringBuilder out = new StringBuilder();
    ExpressionTree variable = node.getVariable();
    ExpressionTree expression = node.getExpression();
    int operator = ((JCTree.JCAssignOp) node).getTag();
    out.append(variable.accept(this, v));
    out.append(" ").append(operatorName(operator)).append(" ");
    out.append(expression.accept(this, v));
    return out.toString();
  }


  public String visitAnnotation(AnnotationTree node, Void v) {
    StringBuilder out = new StringBuilder();
    Tree type = node.getAnnotationType();
    String typeStr = type.accept(this, v);
    if("Override".equals(typeStr)) {
      return "override";
    }
    out.append("@");
    out.append(typeStr);
    List<? extends ExpressionTree> arguments = node.getArguments();
    if (!arguments.isEmpty()) {
      out.append("(");
      boolean first = true;
      for (ExpressionTree arg : arguments) {
        if (!first) {
          out.append(", ");
        }
        first = false;
        if (arg instanceof AssignmentTree) {
          out.append(":");
        }
        out.append(arg.accept(this, v));
      }
      out.append(")");
    }
    return out.toString();
  }


  public String visitUnionType(UnionTypeTree node, Void v) {
    /* handled in the visitCatch()*/
    return null;
  }


  private void pushIndent() {
    ident += TAB_SIZE;
  }


  private void popIndent() {
    ident -= TAB_SIZE;
  }


  private void appendIndent(StringBuilder out) {
    for (int i = 0; i < ident; i += TAB_SIZE) {
      out.append(TAB);
    }
  }


  private void appendAsComment(StringBuilder out, String code) {
    out.append("/*\n");
    pushIndent();
    for (String line : code.split("\\r?\\n")) {
      appendIndent(out);
      out.append(line);
      out.append("\n");
    }
    popIndent();
    appendIndent(out);
    out.append("*/\n");
  }


  public String operatorName(int tag) {
    switch (tag) {
      case JCTree.POS:
        return "+";
      case JCTree.NEG:
        return "-";
      case JCTree.NOT:
        return "!";
      case JCTree.COMPL:
        return "~";
      case JCTree.PREINC:
        return "++";
      case JCTree.PREDEC:
        return "--";
      case JCTree.POSTINC:
        return "++";
      case JCTree.POSTDEC:
        return "--";
      case JCTree.NULLCHK:
        return "<*nullchk*>";
      case JCTree.OR:
        return "or";
      case JCTree.AND:
        return "and";
      case JCTree.BITOR:
        return "|";
      case JCTree.BITXOR:
        return "^";
      case JCTree.BITAND:
        return "&";
      case JCTree.EQ:
        return "==";
      case JCTree.NE:
        return "!=";
      case JCTree.LT:
        return "<";
      case JCTree.GT:
        return ">";
      case JCTree.LE:
        return "<=";
      case JCTree.GE:
        return ">=";
      case JCTree.SL:
        return "<<";
      case JCTree.SR:
        return ">>";
      case JCTree.USR:
        return ">>>";
      case JCTree.PLUS:
        return "+";
      case JCTree.MINUS:
        return "-";
      case JCTree.MUL:
        return "*";
      case JCTree.DIV:
        return "/";
      case JCTree.MOD:
        return "%";
      case JCTree.BITOR_ASG:
        return "|=";
      case JCTree.BITXOR_ASG:
        return "^=";
      case JCTree.BITAND_ASG:
        return "&=";
      case JCTree.SL_ASG:
        return "<<=";
      case JCTree.SR_ASG:
        return ">>=";
      case JCTree.USR_ASG:
        return ">>>=";
      case JCTree.PLUS_ASG:
        return "+=";
      case JCTree.MINUS_ASG:
        return "-=";
      case JCTree.MUL_ASG:
        return "*=";
      case JCTree.DIV_ASG:
        return "/=";
      case JCTree.MOD_ASG:
        return "%=";
      default:
        throw new Error();
    }
  }
}
