package com.cloudbees.groovy.cps.tool;

import com.google.common.io.Resources;
import com.sun.codemodel.CodeWriter;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JExpression;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JOp;
import com.sun.codemodel.JType;
import com.sun.codemodel.JTypeVar;
import com.sun.codemodel.JVar;
import com.sun.source.tree.ArrayAccessTree;
import com.sun.source.tree.ArrayTypeTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.BreakTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.CompoundAssignmentTree;
import com.sun.source.tree.ConditionalExpressionTree;
import com.sun.source.tree.ContinueTree;
import com.sun.source.tree.DoWhileLoopTree;
import com.sun.source.tree.EnhancedForLoopTree;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.ForLoopTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.IfTree;
import com.sun.source.tree.InstanceOfTree;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.PrimitiveTypeTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.ThrowTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.TryTree;
import com.sun.source.tree.TypeCastTree;
import com.sun.source.tree.UnaryTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.tree.WhileLoopTree;
import com.sun.source.tree.WildcardTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.SimpleTreeVisitor;
import com.sun.source.util.Trees;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.TypeVariableSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.code.Types.DefaultSymbolVisitor;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.JCTree.JCIdent;
import groovy.lang.Closure;

import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.NoType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.WildcardType;
import javax.lang.model.util.ElementScanner7;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleTypeVisitor6;
import javax.lang.model.util.Types;
import javax.tools.JavaCompiler.CompilationTask;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import javax.annotation.Generated;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;

/**
 * Generates, for example, {@code CpsDefaultGroovyMethods} from the source code of {@code DefaultGroovyMethods}.
 */
@SuppressWarnings("Since15")
public class Translator {

    private static final Set<String> translatable;
    static {
        try {
            translatable = new HashSet<>(Resources.readLines(Translator.class.getResource("translatable.txt"), StandardCharsets.UTF_8));
        } catch (IOException x) {
            throw new ExceptionInInitializerError(x);
        }
    }

    private final Types types;
    private final Elements elements;
    private final Trees trees;
    private final JavacTask javac;

    private final JCodeModel codeModel = new JCodeModel();

    // class references
    private final JClass $Caller;
    private final JClass $CpsFunction;
    private final JClass $CpsCallableInvocation;
    private final JClass $Builder;
    private final JClass $CatchExpression;
    private final DeclaredType closureType;
    private final Map<String,JClass> otherTranslated = new HashMap<>();
    
    /**
     * To allow sibling calls to overloads to be resolved properly at runtime, write the actual implementation to an overload-proof private method.
     * Example key: {@code $eachByte__byte_array__groovy_lang_Closure}
     */
    private final Map<String, ExecutableElement> overloadsResolved = new TreeMap<>();

    /**
     * Parsed source files.
     */
    private final Iterable<? extends CompilationUnitTree> parsed;

    /**
     * Parse the source code and prepare for translations.
     */
    public Translator(CompilationTask task) throws IOException {
        this.javac = (JavacTask)task;

        $Caller                = codeModel.ref("com.cloudbees.groovy.cps.impl.Caller");
        $CpsFunction           = codeModel.ref("com.cloudbees.groovy.cps.impl.CpsFunction");
        $CpsCallableInvocation = codeModel.ref("com.cloudbees.groovy.cps.impl.CpsCallableInvocation");
        $Builder               = codeModel.ref("com.cloudbees.groovy.cps.Builder");
        $CatchExpression       = codeModel.ref("com.cloudbees.groovy.cps.CatchExpression");

        trees = Trees.instance(javac);
        elements = javac.getElements();
        types = javac.getTypes();
        closureType = types.getDeclaredType(elements.getTypeElement(Closure.class.getName()));

        this.parsed = javac.parse();
        javac.analyze();
    }

    private String mangledName(ExecutableElement e) {
        StringBuilder overloadResolved = new StringBuilder("$").append(n(e));
        e.getParameters().forEach(ve -> {
            overloadResolved.append("__").append(types.erasure(ve.asType()).toString().replace("[]", "_array").replaceAll("[^\\p{javaJavaIdentifierPart}]+", "_"));
        });
        return overloadResolved.toString();
    }

    /**
     * Transforms a single class.
     */
    public void translate(String fqcn, String outfqcn, String sourceJarName) throws JClassAlreadyExistsException {
        final JDefinedClass $output = codeModel._class(outfqcn);
        $output.annotate(Generated.class).param("value", Translator.class.getName()).param("date", new Date().toString()).param("comments", "based on " + sourceJarName);
        $output.annotate(SuppressWarnings.class).param("value", "rawtypes");
        $output.constructor(JMod.PRIVATE);

        CompilationUnitTree dgmCut = getDefaultGroovyMethodCompilationUnitTree(parsed, fqcn);

        overloadsResolved.clear();
        ClassSymbol dgm = (ClassSymbol) elements.getTypeElement(fqcn);
        dgm.accept(new ElementScanner7<Void,Void>() {
            @Override
            public Void visitExecutable(ExecutableElement e, Void __) {
                if (translatable.contains(fqcn + "." + e)) {
                    overloadsResolved.put(mangledName(e), e);
                }
                // System.err.println("Not translating " + e.getAnnotationMirrors() + " " + e.getModifiers() + " " + fqcn + "." + e);
                // TODO else if it is public and has a Closure argument, translate to a form that just throws UnsupportedOperationException when called in CPS mode
                return null;
            }
        },null);
        // TODO verify that we actually found everything listed in translatables
        overloadsResolved.forEach((overloadResolved, e) -> {
            try {
                translateMethod(dgmCut, e, $output, fqcn, overloadResolved);
            } catch (Exception x) {
                throw new RuntimeException("Unable to transform " + fqcn + "." + e, x);
            }
        });

        /*
            private static MethodLocation loc(String methodName) {
                return new MethodLocation(CpsDefaultGroovyMethods.class,methodName);
            }
        */

        JClass $MethodLocation = codeModel.ref("com.cloudbees.groovy.cps.MethodLocation");
        $output.method(JMod.PRIVATE|JMod.STATIC, $MethodLocation, "loc").tap( m -> {
            JVar $methodName = m.param(String.class, "methodName");
            m.body()._return(JExpr._new($MethodLocation).arg($output.dotclass()).arg($methodName));
        });

        // Record the fqcn we've already translated for possible use later.
        otherTranslated.put(fqcn, $output);
    }

    /**
     * @param e
     *      Method in {@code fqcn} to translate.
     */
    private void translateMethod(final CompilationUnitTree cut, ExecutableElement e, JDefinedClass $output, String fqcn, String overloadResolved) {
        String methodName = n(e);
        boolean isPublic = e.getModifiers().contains(Modifier.PUBLIC);

        JMethod delegating = $output.method(isPublic ? JMod.PUBLIC | JMod.STATIC : JMod.STATIC, (JType) null, methodName);
        JMethod m = $output.method(JMod.PRIVATE | JMod.STATIC, (JType) null, overloadResolved);

        Map<String, JTypeVar> typeVars = new HashMap<>();
        e.getTypeParameters().forEach(p -> {
            String name = n(p);
            JTypeVar typeVar = delegating.generify(name);
            JTypeVar typeVar2 = m.generify(name);
            p.getBounds().forEach(b -> {
                JClass binding = (JClass) t(b, typeVars);
                typeVar.bound(binding);
                typeVar2.bound(binding);
            });
            typeVars.put(name, typeVar);
        });
        JType type = t(e.getReturnType(), typeVars);
        delegating.type(type);
        m.type(type);

        List<JVar> delegatingParams = new ArrayList<>();
        List<JVar> params = new ArrayList<>();
        e.getParameters().forEach(p -> {
            JType paramType = t(p.asType(), typeVars);
            delegatingParams.add(e.isVarArgs() && p == e.getParameters().get(e.getParameters().size() - 1) ? delegating.varParam(paramType.elementType(), n(p)) : delegating.param(paramType, n(p)));
            params.add(m.param(paramType, n(p)));
        });

        e.getThrownTypes().forEach(ex -> {
            delegating._throws((JClass)t(ex));
            m._throws((JClass)t(ex));
        });

        boolean returnsVoid = e.getReturnType().getKind() == TypeKind.VOID;

        if (isPublic) {// preamble
            /*
                If the call to this method happen outside CPS code, execute normally via DefaultGroovyMethods
             */
                delegating.body()._if(JOp.cand(
                        JOp.not($Caller.staticInvoke("isAsynchronous").tap(inv -> {
                            inv.arg(delegatingParams.get(0));
                            inv.arg(methodName);
                            for (int i = 1; i < delegatingParams.size(); i++)
                                inv.arg(delegatingParams.get(i));
                        })),
                        JOp.not($Caller.staticInvoke("isAsynchronous")
                                .arg($output.dotclass())
                                .arg(methodName)
                                .args(params))
            ))._then().tap(blk -> {
                JClass $WhateverGroovyMethods  = codeModel.ref(fqcn);
                JInvocation forward = $WhateverGroovyMethods.staticInvoke(methodName).args(delegatingParams);

                if (returnsVoid) {
                    blk.add(forward);
                    blk._return();
                } else {
                    blk._return(forward);
                }
            });
        }

        JInvocation delegateCall = $output.staticInvoke(overloadResolved);
        if (returnsVoid) {
            delegating.body().add(delegateCall);
        } else {
            delegating.body()._return(delegateCall);
        }
        delegatingParams.forEach(p -> delegateCall.arg(p));

        JVar $b = m.body().decl($Builder, "b", JExpr._new($Builder).arg(JExpr.invoke("loc").arg(methodName)).
            invoke("contextualize").arg(codeModel.ref("com.cloudbees.groovy.cps.sandbox.Trusted").staticRef("INSTANCE")));
        JInvocation f = JExpr._new($CpsFunction);

        // parameter names
        f.arg(codeModel.ref(Arrays.class).staticInvoke("asList").tap( inv -> {
            e.getParameters().forEach( p -> inv.arg(n(p)) );
        }));

        // translate the method body into an expression that invokes Builder
        f.arg(trees.getTree(e).getBody().accept(new SimpleTreeVisitor<JExpression,Void>() {
            private JExpression visit(Tree t) {
                if (t==null)    return JExpr._null();
                return visit(t, null);
            }

            /**
             * Maps a symbol to its source location.
             */
            private JExpression loc(Tree t) {
                long pos = trees.getSourcePositions().getStartPosition(cut, t);
                return JExpr.lit((int)cut.getLineMap().getLineNumber(pos));
            }

            @Override
            public JExpression visitWhileLoop(WhileLoopTree wt, Void __) {
                return $b.invoke("while_")
                        .arg(JExpr._null()) // TODO: label
                        .arg(visit(wt.getCondition()))
                        .arg(visit(wt.getStatement()));
            }

            @Override
            public JExpression visitMethodInvocation(MethodInvocationTree mt, Void __) {
                ExpressionTree ms = mt.getMethodSelect();
                JInvocation inv;

                if (ms instanceof MemberSelectTree) {
                    MemberSelectTree mst = (MemberSelectTree) ms;
                    // If this is a call to a static method on another class, it may be an already-translated method,
                    // in which case, we need to use that translated method, not the original. So check if the expression
                    // is an identifier, that it's not the class we're in the process of translating, and if it's one
                    // of the other known translated classes.
                    if (mst.getExpression() instanceof JCIdent &&
                            !((JCIdent)mst.getExpression()).sym.toString().equals(fqcn) &&
                            otherTranslated.containsKey(((JCIdent)mst.getExpression()).sym.toString())) {
                        inv = $b.invoke("functionCall")
                                .arg(loc(mt))
                                .arg($b.invoke("constant").arg(
                                        otherTranslated.get(((JCIdent)mst.getExpression()).sym.toString()).dotclass()))
                                .arg(n(mst.getIdentifier()));

                    } else {
                        inv = $b.invoke("functionCall")
                                .arg(loc(mt))
                                .arg(visit(mst.getExpression()))
                                .arg(n(mst.getIdentifier()));
                    }
                } else
                if (ms instanceof JCIdent) {
                    // invocation without object selection, like  foo(bar,zot)
                    JCIdent it = (JCIdent) ms;
                    if (!it.sym.owner.toString().equals(fqcn)) {
                        if (otherTranslated.containsKey(it.sym.owner.toString())) {
                            // static import from transformed class
                            inv = $b.invoke("functionCall")
                                    .arg(loc(mt))
                                    .arg($b.invoke("constant").arg(otherTranslated.get(it.sym.owner.toString()).dotclass()))
                                    .arg(n(it));
                        } else {
                            // static import from non-transformed class
                            inv = $b.invoke("functionCall")
                                    .arg(loc(mt))
                                    .arg($b.invoke("constant").arg(t(it.sym.owner.type).dotclass()))
                                    .arg(n(it));
                        }
                    } else {
                        // invocation on this class
                        String overloadResolved = mangledName((Symbol.MethodSymbol) it.sym);
                        Optional<? extends Element> callSite = elements.getTypeElement(fqcn).getEnclosedElements().stream().filter(e ->
                            e.getKind() == ElementKind.METHOD && mangledName((ExecutableElement) e).equals(overloadResolved)
                        ).findAny();
                        if (callSite.isPresent()) {
                            ExecutableElement e = (ExecutableElement) callSite.get();
                            if (e.getModifiers().contains(Modifier.PUBLIC) && !e.isVarArgs() && !e.getParameters().stream().anyMatch(p -> types.isAssignable(p.asType(), closureType))) {
                                // Delegate to the standard version.
                                inv = $b.invoke("staticCall")
                                    .arg(loc(mt))
                                    .arg(t(it.sym.owner.type).dotclass())
                                    .arg(n(e));
                            } else if (overloadsResolved.containsKey(overloadResolved)) {
                                // Private, so delegate to our mangled version.
                                // TODO add a String parameter to each internal helper method for the expected methodName to pass to CpsCallableInvocation.<init>
                                // (It could be improved to take a parameter for the name under which we expect methodCall to be invoking it.
                                // Usually just `each`, but might be `$each__java_util_Iterable__groovy_lang_Closure` for the case that one DGM method is delegating to another.
                                // See comment in ContinuationGroup, where we are unable to enforce continuation name mismatches in this case.)
                                inv = $b.invoke("staticCall")
                                    .arg(loc(mt))
                                    .arg($output.dotclass())
                                    .arg(overloadResolved);
                            } else {
                                throw new IllegalStateException("Not yet translating a " + e.getModifiers() + " method; translatable.txt might need to include: " + fqcn + "." + e);
                            }
                        } else {
                            throw new IllegalStateException("Could not find self-call site " + overloadResolved + " for " + mt);
                        }
                    }
                } else {
                    // TODO: figure out what can come here
                    throw new UnsupportedOperationException(ms.toString());
                }

                mt.getArguments().forEach( a -> inv.arg(visit(a)) );
                return inv;
            }

            @Override
            public JExpression visitVariable(VariableTree vt, Void __) {
                return $b.invoke("declareVariable")
                        .arg(loc(vt))
                        .arg(cpsTypeTranslation(erasure(vt)))
                        .arg(n(vt))
                        .arg(visit(vt.getInitializer()));
            }

            @Override
            public JExpression visitIdentifier(IdentifierTree it, Void __) {
                JCIdent idt = (JCIdent) it;
                return idt.sym.accept(new DefaultSymbolVisitor<JExpression, Void>() {
                    @Override
                    public JExpression visitClassSymbol(ClassSymbol cs, Void __) {
                        return $b.invoke("constant").arg(t(cs.asType()).dotclass());
                    }

                    @Override
                    public JExpression visitVarSymbol(VarSymbol s, Void __) {
                        return $b.invoke("localVariable").arg(n(s.name));
                    }

                    @Override
                    public JExpression visitSymbol(Symbol s, Void __) {
                        throw new UnsupportedOperationException(s.toString());
                    }
                }, __);
            }

            @Override
            public JExpression visitBlock(BlockTree bt, Void __) {
                JInvocation inv = $b.invoke("block");
                bt.getStatements().forEach(s -> inv.arg(visit(s)));
                return inv;
            }

            @Override
            public JExpression visitReturn(ReturnTree rt, Void __) {
                return $b.invoke("return_").arg(visit(rt.getExpression()));
            }

            /**
             * When used outside {@link MethodInvocationTree}, this is property access.
             */
            @Override
            public JExpression visitMemberSelect(MemberSelectTree mt, Void __) {
                return $b.invoke("property")
                        .arg(loc(mt))
                        .arg(visit(mt.getExpression()))
                        .arg(n(mt.getIdentifier()));
            }

            @Override
            public JExpression visitTypeCast(TypeCastTree tt, Void __) {
                return $b.invoke("cast")
                        .arg(loc(tt))
                        .arg(visit(tt.getExpression()))
                        .arg(erasure(tt.getType()).dotclass())
                        .arg(JExpr.lit(false));
            }


            @Override
            public JExpression visitIf(IfTree it, Void __) {
                JInvocation inv = $b.invoke("if_")
                        .arg(visit(it.getCondition()))
                        .arg(visit(it.getThenStatement()));
                if (it.getElseStatement()!=null)
                    inv.arg(visit(it.getElseStatement()));
                return inv;
            }

            @Override
            public JExpression visitNewClass(NewClassTree nt, Void __) {
                // TODO: outer class
                if (nt.getEnclosingExpression()!=null)
                    throw new UnsupportedOperationException();

                return $b.invoke("new_").tap(inv -> {
                    inv.arg(loc(nt));
                    inv.arg(cpsTypeTranslation(t(((JCTree) nt).type)));
                    nt.getArguments().forEach( et -> inv.arg(visit(et)) );
                });
            }

            @Override
            public JExpression visitExpressionStatement(ExpressionStatementTree et, Void __) {
                return visit(et.getExpression());
            }

            @Override
            public JExpression visitLiteral(LiteralTree lt, Void __) {
                return $b.invoke("constant").arg(JExpr.literal(lt.getValue()));
            }

            @Override
            public JExpression visitParenthesized(ParenthesizedTree pt, Void __) {
                return visit(pt.getExpression());
            }

            @Override
            public JExpression visitBinary(BinaryTree bt, Void __) {
                return $b.invoke(opName(bt.getKind()))
                        .arg(loc(bt))
                        .arg(visit(bt.getLeftOperand()))
                        .arg(visit(bt.getRightOperand()));
            }

            @Override
            public JExpression visitUnary(UnaryTree ut, Void __) {
                return $b.invoke(opName(ut.getKind()))
                        .arg(loc(ut))
                        .arg(visit(ut.getExpression()));
            }

            @Override
            public JExpression visitCompoundAssignment(CompoundAssignmentTree ct, Void __) {
                return $b.invoke(opName(ct.getKind()))
                        .arg(loc(ct))
                        .arg(visit(ct.getVariable()))
                        .arg(visit(ct.getExpression()));
            }

            private String opName(Kind kind) {
                switch (kind) {
                case EQUAL_TO:              return "compareEqual";
                case NOT_EQUAL_TO:          return "compareNotEqual";
                case LESS_THAN_EQUAL:       return "lessThanEqual";
                case LESS_THAN:             return "lessThan";
                case GREATER_THAN_EQUAL:    return "greaterThanEqual";
                case GREATER_THAN:          return "greaterThan";
                case PREFIX_INCREMENT:      return "prefixInc";
                case POSTFIX_INCREMENT:     return "postfixInc";
                case POSTFIX_DECREMENT:     return "postfixDec";
                case LOGICAL_COMPLEMENT:    return "not";
                case CONDITIONAL_OR:        return "logicalOr";
                case CONDITIONAL_AND:       return "logicalAnd";
                case PLUS:                  return "plus";
                case PLUS_ASSIGNMENT:       return "plusEqual";
                case MINUS:                 return "minus";
                case MINUS_ASSIGNMENT:      return "minusEqual";
                }
                throw new UnsupportedOperationException(kind.toString());
            }

            @Override
            public JExpression visitAssignment(AssignmentTree at, Void __) {
                return $b.invoke("assign")
                        .arg(loc(at))
                        .arg(visit(at.getVariable()))
                        .arg(visit(at.getExpression()));
            }

            /**
             * This is needed to handle cases like {@code Object[].class}.
             */
            @Override
            public JExpression visitArrayType(ArrayTypeTree at, Void __) {
                if (at.getType() instanceof IdentifierTree) {
                    return visitIdentifier((IdentifierTree) at.getType(), __);
                } else {
                    return defaultAction(at, __);
                }
            }

            @Override
            public JExpression visitNewArray(NewArrayTree nt, Void __) {
                if (nt.getInitializers()!=null) {
                    return $b.invoke("newArrayFromInitializers").tap(inv -> {
                        nt.getInitializers().forEach(d -> inv.arg(visit(d)));
                    });
                } else {
                    return $b.invoke("newArray").tap(inv -> {
                        inv.arg(loc(nt));
                        inv.arg(t(nt.getType()).dotclass());
                        nt.getDimensions().forEach(d -> inv.arg(visit(d)));
                    });
                }
            }

            @Override
            public JExpression visitForLoop(ForLoopTree ft, Void __) {
                return $b.invoke("forLoop")
                        .arg(JExpr._null())
                        .arg($b.invoke("sequence").tap(inv -> ft.getInitializer().forEach(i -> inv.arg(visit(i)))))
                        .arg(visit(ft.getCondition()))
                        .arg($b.invoke("sequence").tap(inv -> ft.getUpdate().forEach(i -> inv.arg(visit(i)))))
                        .arg(visit(ft.getStatement()));
            }

            @Override
            public JExpression visitEnhancedForLoop(EnhancedForLoopTree et, Void __) {
                return $b.invoke("forInLoop")
                        .arg(loc(et))
                        .arg(JExpr._null())
                        .arg(erasure(et.getVariable()).dotclass())
                        .arg(n(et.getVariable()))
                        .arg(visit(et.getExpression()))
                        .arg(visit(et.getStatement()));
            }

            @Override
            public JExpression visitArrayAccess(ArrayAccessTree at, Void __) {
                return $b.invoke("array")
                        .arg(loc(at))
                        .arg(visit(at.getExpression()))
                        .arg(visit(at.getIndex()));
            }

            @Override
            public JExpression visitBreak(BreakTree node, Void __) {
                if (node.getLabel()!=null)
                    throw new UnsupportedOperationException();
                return $b.invoke("break_").arg(JExpr._null());
            }

            @Override
            public JExpression visitContinue(ContinueTree node, Void aVoid) {
                if (node.getLabel()!=null)
                    throw new UnsupportedOperationException();
                return $b.invoke("continue_").arg(JExpr._null());
            }

            @Override
            public JExpression visitInstanceOf(InstanceOfTree it, Void __) {
                return $b.invoke("instanceOf")
                        .arg(loc(it))
                        .arg(visit(it.getExpression()))
                        .arg($b.invoke("constant").arg(t(it.getType()).dotclass()));
            }

            @Override
            public JExpression visitThrow(ThrowTree tt, Void __) {
                return $b.invoke("throw_")
                        .arg(loc(tt))
                        .arg(visit(tt.getExpression()));
            }

            @Override
            public JExpression visitDoWhileLoop(DoWhileLoopTree dt, Void __) {
                return $b.invoke("doWhile")
                        .arg(JExpr._null())
                        .arg(visit(dt.getStatement()))
                        .arg(visit(dt.getCondition()));
            }

            @Override
            public JExpression visitConditionalExpression(ConditionalExpressionTree ct, Void __) {
                return $b.invoke("ternaryOp")
                        .arg(visit(ct.getCondition()))
                        .arg(visit(ct.getTrueExpression()))
                        .arg(visit(ct.getFalseExpression()));
            }

            @Override
            public JExpression visitTry(TryTree tt, Void __) {
                return $b.invoke("tryCatch")
                        .arg(visit(tt.getBlock()))
                        .arg(visit(tt.getFinallyBlock()))
                        .tap(inv ->
                            tt.getCatches().forEach(ct ->
                                JExpr._new($CatchExpression)
                                    .arg(t(ct.getParameter()).dotclass())
                                    .arg(n(ct.getParameter()))
                                    .arg(visit(ct.getBlock())))
                        );
            }

            @Override
            protected JExpression defaultAction(Tree node, Void aVoid) {
                throw new UnsupportedOperationException(node.toString());
            }
        }, null));

        JVar $f = m.body().decl($CpsFunction, "f", f);
        m.body()._throw(JExpr._new($CpsCallableInvocation)
            .arg(JExpr.lit(methodName))
            .arg($f)
            .arg(JExpr._null())
            .args(params));
    }

    private CompilationUnitTree getDefaultGroovyMethodCompilationUnitTree(Iterable<? extends CompilationUnitTree> parsed, String fqcn) {
        for (CompilationUnitTree cut : parsed) {
            for (Tree t : cut.getTypeDecls()) {
                if (t.getKind() == Kind.CLASS) {
                    ClassTree ct = (ClassTree)t;
                    if (ct.getSimpleName().toString().equals(fqcn.replaceFirst("^.+[.]", ""))) { // TODO how do we get the FQCN of a ClassTree?
                        return cut;
                    }
                }
            }
        }
        throw new IllegalStateException(fqcn + " wasn't parsed");
    }


    /**
     * Convert a type representation from javac to codemodel.
     */
    private JType t(Tree t) {
        return t.accept(new TypeTranslator(), null);
    }

    /**
     * Converts a type representation to its erasure.
     */
    private JType erasure(Tree t) {
        return t.accept(new TypeTranslator() {
            @Override
            public JType visitParameterizedType(ParameterizedTypeTree pt, Void __) {
                return visit(pt.getType());
            }

            @Override
            public JType visitWildcard(WildcardTree wt, Void __) {
                Tree b = wt.getBound();
                if (b==null)    return codeModel.ref(Object.class);
                else            return visit(b);
            }

            @Override
            public JType visitIdentifier(IdentifierTree it, Void __) {
                JCIdent idt = (JCIdent) it;
                if (idt.sym instanceof ClassSymbol) {
                    ClassSymbol cs = (ClassSymbol) idt.sym;
                    return codeModel.ref(cs.className());
                }
                if (idt.sym instanceof TypeVariableSymbol) {
                    TypeVariableSymbol tcs = (TypeVariableSymbol) idt.sym;
                    if (tcs.getBounds().isEmpty())
                        return codeModel.ref(Object.class);
                    else
                        return t(tcs.getBounds().get(0));
                }
                throw new UnsupportedOperationException(idt.sym.toString());
            }
        }, null);
    }

    private JType t(TypeMirror m) {
        return t(m, Collections.emptyMap());
    }

    private JType t(TypeMirror m, Map<String, JTypeVar> typeVars) {
        if (m.getKind().isPrimitive())
            return JType.parse(codeModel,m.toString());

        return m.accept(new SimpleTypeVisitor6<JType, Void>() {
            @Override
            public JType visitPrimitive(PrimitiveType t, Void __) {
                return primitive(t, t.getKind());
            }

            @Override
            public JType visitDeclared(DeclaredType t, Void __) {
                String name = n(((TypeElement) t.asElement()).getQualifiedName());
                if (name.isEmpty())
                    throw new UnsupportedOperationException("Anonymous class: "+t);
                JClass base = codeModel.ref(name);
                if (t.getTypeArguments().isEmpty())
                    return base;

                List<JClass> typeArgs = new ArrayList<>();
                t.getTypeArguments().forEach(a -> typeArgs.add((JClass) t(a, typeVars)));
                return base.narrow(typeArgs);
            }

            @Override
            public JType visitTypeVariable(TypeVariable t, Void __) {
                String name = t.asElement().getSimpleName().toString();
                JTypeVar var = typeVars.get(name);
                if (var != null) {
                    return var; // TODO bounds
                } else {
                    // TODO <T,U>with(U,groovy.lang.Closure<T>) somehow asks us to visit V, huh?
                    return t(t.getUpperBound(), typeVars);
                }
            }

            @Override
            public JType visitNoType(NoType t, Void __) {
                return primitive(t, t.getKind());
            }

            @Override
            public JType visitArray(ArrayType t, Void __) {
                return t(t.getComponentType(), typeVars).array();
            }

            @Override
            public JType visitWildcard(WildcardType t, Void aVoid) {
                if (t.getExtendsBound()!=null) {
                    return t(t.getExtendsBound(), typeVars).boxify().wildcard();
                }
                if (t.getSuperBound()!=null) {
                    throw new UnsupportedOperationException();
                }
                return codeModel.wildcard();
            }

            @Override
            protected JType defaultAction(TypeMirror e, Void __) {
                throw new UnsupportedOperationException(e.toString());
            }
        }, null);
    }

    private String n(Element e) {
        return e.getSimpleName().toString();
    }
    private String n(Name n) {
        return n.toString();
    }
    private String n(VariableTree v) {
        return n(v.getName());
    }
    private String n(IdentifierTree v) {
        return n(v.getName());
    }

    private JType primitive(Object src, TypeKind k) {
        switch (k) {
        case BOOLEAN:   return codeModel.BOOLEAN;
        case BYTE:      return codeModel.BYTE;
        case SHORT:     return codeModel.SHORT;
        case INT:       return codeModel.INT;
        case LONG:      return codeModel.LONG;
        case CHAR:      return codeModel.CHAR;
        case FLOAT:     return codeModel.FLOAT;
        case DOUBLE:    return codeModel.DOUBLE;
        case VOID:      return codeModel.VOID;
        }
        throw new UnsupportedOperationException(src.toString());
    }

    /**
     * Replaces non-serializable types with CPS-specific variants.
     *
     * @param original a {@link JType} to inspect
     * @return The {@link JType#dotclass()} for either the original {@link JType} or for the CPS equivalent.
     */
    private JExpression cpsTypeTranslation(JType original) {
        if (original.fullName().equals("org.codehaus.groovy.runtime.callsite.BooleanClosureWrapper")) {
            return codeModel.ref("com.cloudbees.groovy.cps.impl.CpsBooleanClosureWrapper").dotclass();
        } else {
            return original.dotclass();
        }
    }

    /**
     * Generate transalted result into source files.
     */
    public void generateTo(CodeWriter cw) throws IOException {
        codeModel.build(cw);
    }

    private class TypeTranslator extends SimpleTreeVisitor<JType, Void> {
        protected JType visit(Tree t) {
            return visit(t,null);
        }

        @Override
        public JType visitVariable(VariableTree node, Void __) {
            return visit(node.getType());
        }

        @Override
        public JType visitParameterizedType(ParameterizedTypeTree pt, Void __) {
            JClass base = (JClass)visit(pt.getType());
            List<JClass> args = new ArrayList<>();
            for (Tree arg : pt.getTypeArguments()) {
                args.add((JClass)visit(arg));
            }
            return base.narrow(args);
        }

        @Override
        public JType visitIdentifier(IdentifierTree it, Void __) {
            JCIdent idt = (JCIdent) it;
            return codeModel.ref(idt.sym.toString());
        }

        @Override
        public JType visitPrimitiveType(PrimitiveTypeTree pt, Void aVoid) {
            return primitive(pt, pt.getPrimitiveTypeKind());
        }

        @Override
        public JType visitArrayType(ArrayTypeTree at, Void __) {
            return visit(at.getType()).array();
        }

        /**
         * Nested type
         */
        @Override
        public JType visitMemberSelect(MemberSelectTree mt, Void __) {
            return t(((JCFieldAccess)mt).type);
        }

        @Override
        public JType visitWildcard(WildcardTree wt, Void __) {
            Tree b = wt.getBound();
            if (b==null)    return codeModel.wildcard();
            else            return visit(b).boxify().wildcard();
        }

        @Override
        protected JType defaultAction(Tree node, Void __) {
            throw new UnsupportedOperationException(node.toString());
        }
    }
}