/*
 ***** BEGIN LICENSE BLOCK *****
 * Version: CPL 1.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Common Public
 * License Version 1.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.eclipse.org/legal/cpl-v10.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 * 
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the CPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the CPL, the GPL or the LGPL.
 ***** END LICENSE BLOCK *****/

package org.jruby.compiler.impl;

import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyFloat;
import org.jruby.RubyInstanceConfig;
import org.jruby.RubyModule;
import org.jruby.compiler.ArgumentsCallback;
import org.jruby.compiler.BodyCompiler;
import org.jruby.compiler.CompilerCallback;
import org.jruby.compiler.InvocationCompiler;
import org.jruby.compiler.NotCompilableException;
import org.jruby.internal.runtime.methods.DynamicMethod;
import org.jruby.runtime.Block;
import org.jruby.runtime.CallSite;
import org.jruby.runtime.CallType;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import static org.jruby.util.CodegenUtils.*;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;

/**
 *
 * @author headius
 */
public class StandardInvocationCompiler implements InvocationCompiler {
    protected BaseBodyCompiler methodCompiler;
    protected SkinnyMethodAdapter method;

    public StandardInvocationCompiler(BaseBodyCompiler methodCompiler, SkinnyMethodAdapter method) {
        this.methodCompiler = methodCompiler;
        this.method = method;
    }

    public SkinnyMethodAdapter getMethodAdapter() {
        return this.method;
    }

    public void setMethodAdapter(SkinnyMethodAdapter sma) {
        this.method = sma;
    }

    public void invokeAttrAssignMasgn(String name, CompilerCallback receiverCallback, final ArgumentsCallback argsCallback, boolean selfCall) {
        // value is already on stack, save it for later
        final int temp = methodCompiler.getVariableCompiler().grabTempLocal();
        methodCompiler.getVariableCompiler().setTempLocal(temp);
        
        ArgumentsCallback newArgumentsCallback = new ArgumentsCallback() {
            public int getArity() {
                return (argsCallback == null) ? 1 : argsCallback.getArity() + 1;
            }

            public void call(BodyCompiler context) {
                if (argsCallback != null) argsCallback.call(context);
                methodCompiler.getVariableCompiler().getTempLocal(temp);
            }
        };
        
        invokeAttrAssign(name, receiverCallback, newArgumentsCallback, selfCall, true);
    }

    public void invokeAttrAssign(String name, CompilerCallback receiverCallback, ArgumentsCallback argsCallback, boolean isSelf, boolean expr) {
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, name, isSelf ? CallType.VARIABLE : CallType.NORMAL);

        methodCompiler.loadThreadContext(); // [adapter, tc]

        // for visibility checking without requiring frame self
        // TODO: don't bother passing when fcall or vcall, and adjust callsite appropriately
        methodCompiler.loadSelf();
        
        if (receiverCallback != null) {
            receiverCallback.call(methodCompiler);
        } else {
            methodCompiler.loadSelf();
        }
        
        String signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class));
        argsCallback.call(methodCompiler);
        int tmp = methodCompiler.getVariableCompiler().grabTempLocal();
        
        switch (argsCallback.getArity()) {
        case 1:
            signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class));
            if (expr) {
                method.dup();
                method.astore(tmp);
            }
            break;
        case 2:
            signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class));
            if (expr) {
                method.dup();
                method.astore(tmp);
            }
            break;
        case 3:
            signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class));
            if (expr) {
                method.dup();
                method.astore(tmp);
            }
            break;
        default:
            signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject[].class));
            if (expr) {
                method.dup();
                methodCompiler.invokeUtilityMethod("lastElement", sig(IRubyObject.class, IRubyObject[].class));
                method.astore(tmp);
            }
        }
        
        // invoke
        method.invokevirtual(p(CallSite.class), "call", signature);
        
        // restore incoming value if expression
        method.pop();
        if (expr) method.aload(tmp);
        methodCompiler.getVariableCompiler().releaseTempLocal();
    }
    
    public void opElementAsgnWithOr(CompilerCallback receiver, ArgumentsCallback args, CompilerCallback valueCallback) {
        // get call site and thread context
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, "[]", CallType.FUNCTIONAL);
        methodCompiler.loadThreadContext();
        methodCompiler.loadSelf();
        
        // evaluate and save receiver and args
        receiver.call(methodCompiler);
        args.call(methodCompiler);
        method.dup2();
        int argsLocal = methodCompiler.getVariableCompiler().grabTempLocal();
        methodCompiler.getVariableCompiler().setTempLocal(argsLocal);
        int receiverLocal = methodCompiler.getVariableCompiler().grabTempLocal();
        methodCompiler.getVariableCompiler().setTempLocal(receiverLocal);
        
        // invoke
        switch (args.getArity()) {
        case 1:
            method.invokevirtual(p(CallSite.class), "call", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class));
            break;
        default:
            method.invokevirtual(p(CallSite.class), "call", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject[].class));
        }
        
        // check if it's true, ending if so
        method.dup();
        methodCompiler.invokeIRubyObject("isTrue", sig(boolean.class));
        Label done = new Label();
        method.ifne(done);
        
        // not true, eval value and assign
        method.pop();
        // thread context, receiver and original args
        methodCompiler.loadThreadContext();
        methodCompiler.loadSelf();
        methodCompiler.getVariableCompiler().getTempLocal(receiverLocal);
        methodCompiler.getVariableCompiler().getTempLocal(argsLocal);
        
        // eval value for assignment
        valueCallback.call(methodCompiler);
        
        // call site
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, "[]=", CallType.FUNCTIONAL);
        
        // depending on size of original args, call appropriate utility method
        switch (args.getArity()) {
        case 0:
            throw new NotCompilableException("Op Element Asgn with zero-arity args");
        case 1:
            methodCompiler.invokeUtilityMethod("opElementAsgnWithOrPartTwoOneArg", 
                    sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, CallSite.class));
            break;
        case 2:
            methodCompiler.invokeUtilityMethod("opElementAsgnWithOrPartTwoTwoArgs", 
                    sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject[].class, IRubyObject.class, CallSite.class));
            break;
        case 3:
            methodCompiler.invokeUtilityMethod("opElementAsgnWithOrPartTwoThreeArgs", 
                    sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject[].class, IRubyObject.class, CallSite.class));
            break;
        default:
            methodCompiler.invokeUtilityMethod("opElementAsgnWithOrPartTwoNArgs", 
                    sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject[].class, IRubyObject.class, CallSite.class));
            break;
        }
        
        method.label(done);
        
        methodCompiler.getVariableCompiler().releaseTempLocal();
        methodCompiler.getVariableCompiler().releaseTempLocal();
    }
    
    public void opElementAsgnWithAnd(CompilerCallback receiver, ArgumentsCallback args, CompilerCallback valueCallback) {
        // get call site and thread context
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, "[]", CallType.FUNCTIONAL);
        methodCompiler.loadThreadContext();
        methodCompiler.loadSelf();
        
        // evaluate and save receiver and args
        receiver.call(methodCompiler);
        args.call(methodCompiler);
        method.dup2();
        int argsLocal = methodCompiler.getVariableCompiler().grabTempLocal();
        methodCompiler.getVariableCompiler().setTempLocal(argsLocal);
        int receiverLocal = methodCompiler.getVariableCompiler().grabTempLocal();
        methodCompiler.getVariableCompiler().setTempLocal(receiverLocal);
        
        // invoke
        switch (args.getArity()) {
        case 1:
            method.invokevirtual(p(CallSite.class), "call", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class));
            break;
        default:
            method.invokevirtual(p(CallSite.class), "call", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject[].class));
        }
        
        // check if it's true, ending if not
        method.dup();
        methodCompiler.invokeIRubyObject("isTrue", sig(boolean.class));
        Label done = new Label();
        method.ifeq(done);
        
        // not true, eval value and assign
        method.pop();
        // thread context, receiver and original args
        methodCompiler.loadThreadContext();
        methodCompiler.loadSelf();
        methodCompiler.getVariableCompiler().getTempLocal(receiverLocal);
        methodCompiler.getVariableCompiler().getTempLocal(argsLocal);
        
        // eval value and save it
        valueCallback.call(methodCompiler);
        
        // call site
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, "[]=", CallType.FUNCTIONAL);
        
        // depending on size of original args, call appropriate utility method
        switch (args.getArity()) {
        case 0:
            throw new NotCompilableException("Op Element Asgn with zero-arity args");
        case 1:
            methodCompiler.invokeUtilityMethod("opElementAsgnWithOrPartTwoOneArg", 
                    sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, CallSite.class));
            break;
        case 2:
            methodCompiler.invokeUtilityMethod("opElementAsgnWithOrPartTwoTwoArgs", 
                    sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject[].class, IRubyObject.class, CallSite.class));
            break;
        case 3:
            methodCompiler.invokeUtilityMethod("opElementAsgnWithOrPartTwoThreeArgs", 
                    sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject[].class, IRubyObject.class, CallSite.class));
            break;
        default:
            methodCompiler.invokeUtilityMethod("opElementAsgnWithOrPartTwoNArgs", 
                    sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject[].class, IRubyObject.class, CallSite.class));
            break;
        }
        
        method.label(done);
        
        methodCompiler.getVariableCompiler().releaseTempLocal();
        methodCompiler.getVariableCompiler().releaseTempLocal();
    }
    
    public void opElementAsgnWithMethod(CompilerCallback receiver, ArgumentsCallback args, CompilerCallback valueCallback, String operator) {
        methodCompiler.loadThreadContext();
        methodCompiler.loadSelf();
        receiver.call(methodCompiler);
        args.call(methodCompiler);
        valueCallback.call(methodCompiler); // receiver, args, result, value
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, "[]", CallType.FUNCTIONAL);
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, operator, CallType.NORMAL);
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, "[]=", CallType.FUNCTIONAL);
        
        switch (args.getArity()) {
        case 0:
            methodCompiler.invokeUtilityMethod("opElementAsgnWithMethod",
                    sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, CallSite.class, CallSite.class, CallSite.class));
            break;
        case 1:
            methodCompiler.invokeUtilityMethod("opElementAsgnWithMethod",
                    sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, CallSite.class, CallSite.class, CallSite.class));
            break;
        case 2:
            methodCompiler.invokeUtilityMethod("opElementAsgnWithMethod",
                    sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, CallSite.class, CallSite.class, CallSite.class));
            break;
        case 3:
            methodCompiler.invokeUtilityMethod("opElementAsgnWithMethod",
                    sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, CallSite.class, CallSite.class, CallSite.class));
            break;
        default:
            methodCompiler.invokeUtilityMethod("opElementAsgnWithMethod",
                    sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject[].class, IRubyObject.class, CallSite.class, CallSite.class, CallSite.class));
            break;
        }
    }

    public void invokeBinaryFixnumRHS(String name, CompilerCallback receiverCallback, long fixnum) {
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, name, CallType.NORMAL);
        methodCompiler.loadThreadContext(); // [adapter, tc]

        // for visibility checking without requiring frame self
        // TODO: don't bother passing when fcall or vcall, and adjust callsite appropriately
        methodCompiler.loadSelf();

        if (receiverCallback != null) {
            receiverCallback.call(methodCompiler);
        } else {
            methodCompiler.loadSelf();
        }

        method.ldc(fixnum);

        String signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, long.class));
        String callSiteMethod = "call";

        method.invokevirtual(p(CallSite.class), callSiteMethod, signature);
    }

    public void invokeBinaryBooleanFixnumRHS(String name, CompilerCallback receiverCallback, long fixnum) {
        invokeBinaryFixnumRHS(name, receiverCallback, fixnum);
        
        methodCompiler.isTrue();
    }

    public void invokeBinaryFloatRHS(String name, CompilerCallback receiverCallback, double flote) {
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, name, CallType.NORMAL);
        methodCompiler.loadThreadContext(); // [adapter, tc]

        // for visibility checking without requiring frame self
        // TODO: don't bother passing when fcall or vcall, and adjust callsite appropriately
        methodCompiler.loadSelf();

        if (receiverCallback != null) {
            receiverCallback.call(methodCompiler);
        } else {
            methodCompiler.loadSelf();
        }

        method.ldc(flote);

        String signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, double.class));
        String callSiteMethod = "call";

        method.invokevirtual(p(CallSite.class), callSiteMethod, signature);
    }

    public void invokeFixnumLong(String rubyName, int moduleGeneration, CompilerCallback receiverCallback, String methodName, long fixnum) {
        receiverCallback.call(methodCompiler);
        final int tmp = methodCompiler.getVariableCompiler().grabTempLocal();
        method.astore(tmp);

        Label slow = new Label();
        Label after = new Label();
        if (!RubyInstanceConfig.NOGUARDS_COMPILE_ENABLED) {
            method.aload(tmp);
            method.ldc(moduleGeneration);
            methodCompiler.invokeUtilityMethod("isGenerationEqual", sig(boolean.class, IRubyObject.class, int.class));

            method.ifeq(slow);
        }

        method.aload(tmp);
        method.checkcast(p(RubyFixnum.class));
        methodCompiler.loadThreadContext();
        method.ldc(fixnum);

        method.invokevirtual(p(RubyFixnum.class), methodName, sig(IRubyObject.class, ThreadContext.class, long.class));

        if (!RubyInstanceConfig.NOGUARDS_COMPILE_ENABLED) {
            method.go_to(after);
            method.label(slow);

            invokeBinaryFixnumRHS(rubyName, new CompilerCallback() {
                public void call(BodyCompiler context) {
                    method.aload(tmp);
                }
            }, fixnum);

            method.label(after);
        }
        methodCompiler.getVariableCompiler().releaseTempLocal();
    }

    public void invokeFloatDouble(String rubyName, int moduleGeneration, CompilerCallback receiverCallback, String methodName, double flote) {
        receiverCallback.call(methodCompiler);
        final int tmp = methodCompiler.getVariableCompiler().grabTempLocal();
        method.astore(tmp);
        
        Label slow = new Label();
        Label after = new Label();
        if (!RubyInstanceConfig.NOGUARDS_COMPILE_ENABLED) {
            method.aload(tmp);
            method.ldc(moduleGeneration);
            methodCompiler.invokeUtilityMethod("isGenerationEqual", sig(boolean.class, IRubyObject.class, int.class));

            method.ifeq(slow);
        }

        method.aload(tmp);
        method.checkcast(p(RubyFloat.class));
        methodCompiler.loadThreadContext();
        method.ldc(flote);

        method.invokevirtual(p(RubyFloat.class), methodName, sig(IRubyObject.class, ThreadContext.class, double.class));

        if (!RubyInstanceConfig.NOGUARDS_COMPILE_ENABLED) {
            method.go_to(after);
            method.label(slow);

            invokeBinaryFloatRHS(rubyName, new CompilerCallback() {
                public void call(BodyCompiler context) {
                    method.aload(tmp);
                }
            }, flote);

            method.label(after);
        }
        methodCompiler.getVariableCompiler().releaseTempLocal();
    }

    public void invokeRecursive(String name, int moduleGeneration, ArgumentsCallback argsCallback, CompilerCallback closure, CallType callType, boolean iterator) {
        if (methodCompiler.getVariableCompiler().isHeap()) {
            // direct recursive invocation doesn't work with heap-based scopes yet
            invokeDynamic(name, null, argsCallback, callType, closure, iterator);
        } else {
            Label slow = new Label();
            Label after = new Label();
            if (!RubyInstanceConfig.NOGUARDS_COMPILE_ENABLED) {
                methodCompiler.loadSelf();
                method.ldc(moduleGeneration);
                methodCompiler.invokeUtilityMethod("isGenerationEqual", sig(boolean.class, IRubyObject.class, int.class));

                method.ifeq(slow);
            }

            method.aload(0);
            methodCompiler.loadThreadContext();
            methodCompiler.loadSelf();
            if (argsCallback != null) {
                argsCallback.call(methodCompiler);
            }
            method.aconst_null();

            method.invokestatic(methodCompiler.getScriptCompiler().getClassname(), methodCompiler.getNativeMethodName(), methodCompiler.getSignature());

            if (!RubyInstanceConfig.NOGUARDS_COMPILE_ENABLED) {
                method.go_to(after);
                method.label(slow);

                invokeDynamic(name, null, argsCallback, callType, closure, iterator);

                method.label(after);
            }
        }
    }

    public void invokeNative(String name, DynamicMethod.NativeCall nativeCall,
            int moduleGeneration, CompilerCallback receiver, final ArgumentsCallback args,
            CompilerCallback closure, CallType callType, boolean iterator) {
        Class[] nativeSignature = nativeCall.getNativeSignature();

        int leadingArgs = 0;
        receiver.call(methodCompiler);
        final int tmp = methodCompiler.getVariableCompiler().grabTempLocal();
        method.astore(tmp);

        int[] _argTmp = null;
        if (args != null) {
            args.call(methodCompiler);
            switch (args.getArity()) {
                case 3:
                    _argTmp = new int[3];
                    method.astore(_argTmp[2] = methodCompiler.getVariableCompiler().grabTempLocal());
                case 2:
                    if (_argTmp == null) _argTmp = new int[2];
                    method.astore(_argTmp[1] = methodCompiler.getVariableCompiler().grabTempLocal());
                case 1:
                default:
                    if (_argTmp == null) _argTmp = new int[1];
                    method.astore(_argTmp[0] = methodCompiler.getVariableCompiler().grabTempLocal());
            }
            leadingArgs += args.getArity();
        }
        final int[] argTmp = _argTmp;

        // validate generation
        Label slow = new Label();
        Label after = new Label();
        if (!RubyInstanceConfig.NOGUARDS_COMPILE_ENABLED) {
            method.aload(tmp);
            method.ldc(moduleGeneration);
            methodCompiler.invokeUtilityMethod("isGenerationEqual", sig(boolean.class, IRubyObject.class, int.class));

            method.ifeq(slow);
        }

        if (nativeCall.isStatic()) {
            if (nativeSignature.length > 0 && nativeSignature[0] == ThreadContext.class) {
                methodCompiler.loadThreadContext();
                leadingArgs++;
            }
            method.aload(tmp);
            leadingArgs++;
        } else {
            method.aload(tmp);
            method.checkcast(p(nativeCall.getNativeTarget()));
            if (nativeSignature.length > 0 && nativeSignature[0] == ThreadContext.class) {
                methodCompiler.loadThreadContext();
                leadingArgs++;
            }
        }

        if (args != null) {
            switch (args.getArity()) {
                case 1:
                default:
                    method.aload(argTmp[0]);
                    break;
                case 2:
                    method.aload(argTmp[0]);
                    method.aload(argTmp[1]);
                    break;
                case 3:
                    method.aload(argTmp[0]);
                    method.aload(argTmp[1]);
                    method.aload(argTmp[2]);
                    break;
            }
        }

        if (closure != null) {
            closure.call(methodCompiler);
            if (nativeSignature.length == leadingArgs + 1 && nativeSignature[leadingArgs] == Block.class) {
                // ok, pass the block
            } else {
                // doesn't receive block, drop it
                // note: have to evaluate it because it could be a & block arg
                // with side effects
                method.pop();
            }
        } else {
            if (nativeSignature.length == leadingArgs + 1 && nativeSignature[leadingArgs] == Block.class) {
                // needs a block
                method.getstatic(p(Block.class), "NULL_BLOCK", ci(Block.class));
            } else {
                // ok with no block
            }
        }

        if (nativeCall.isStatic()) {
            method.invokestatic(p(nativeCall.getNativeTarget()), nativeCall.getNativeName(), sig(nativeCall.getNativeReturn(), nativeSignature));
        } else {
            method.invokevirtual(p(nativeCall.getNativeTarget()), nativeCall.getNativeName(), sig(nativeCall.getNativeReturn(), nativeSignature));
        }

        if (!RubyInstanceConfig.NOGUARDS_COMPILE_ENABLED) {
            method.go_to(after);
            method.label(slow);

            ArgumentsCallback newArgs = null;
            if (args != null) {
                newArgs = new ArgumentsCallback() {
                    public int getArity() {
                        return args.getArity();
                    }

                    public void call(BodyCompiler context) {
                        switch (args.getArity()) {
                            case 1:
                            default:
                                method.aload(argTmp[0]);
                                break;
                            case 2:
                                method.aload(argTmp[0]);
                                method.aload(argTmp[1]);
                                break;
                            case 3:
                                method.aload(argTmp[0]);
                                method.aload(argTmp[1]);
                                method.aload(argTmp[2]);
                                break;
                        }
                    }
                };
            }
            invokeDynamic(name, new CompilerCallback() {
                public void call(BodyCompiler context) {
                    method.aload(tmp);
                }
            }, newArgs, callType, closure, iterator);

            method.label(after);
        }

        methodCompiler.getVariableCompiler().releaseTempLocal();
        if (argTmp != null) {
            for (int i : argTmp) methodCompiler.getVariableCompiler().releaseTempLocal();
        }
    }

    public void invokeTrivial(String name, int moduleGeneration, CompilerCallback body) {
        // validate generation
        Label slow = new Label();
        Label after = new Label();
        if (!RubyInstanceConfig.NOGUARDS_COMPILE_ENABLED) {
            methodCompiler.loadSelf();
            method.ldc(moduleGeneration);
            methodCompiler.invokeUtilityMethod("isGenerationEqual", sig(boolean.class, IRubyObject.class, int.class));

            method.iffalse(slow);
        }

        body.call(methodCompiler);

        if (!RubyInstanceConfig.NOGUARDS_COMPILE_ENABLED) {
            method.go_to(after);
            method.label(slow);
            
            invokeDynamic(name, null, null, CallType.FUNCTIONAL, null, false);

            method.label(after);
        }
    }
    
    public void invokeDynamic(String name, CompilerCallback receiverCallback, ArgumentsCallback argsCallback, CallType callType, CompilerCallback closureArg, boolean iterator) {
        if (RubyInstanceConfig.INLINE_DYNCALL_ENABLED && closureArg == null) {
            if (callType == CallType.FUNCTIONAL || callType == CallType.VARIABLE) {
                if (argsCallback == null) {
                    invokeDynamicSelfNoBlockZero(name);
                    return;
                } else if (argsCallback.getArity() >= 1 && argsCallback.getArity() <= 3) {
                    invokeDynamicSelfNoBlockSpecificArity(name, argsCallback);
                    return;
                }
//            } else if (callType == CallType.NORMAL) {
//                if (argsCallback == null) {
//                    invokeDynamicNoBlockZero(name, receiverCallback);
//                    return;
//                } else if (argsCallback.getArity() >= 1 && argsCallback.getArity() <= 3) {
//                    invokeDynamicNoBlockSpecificArity(name, receiverCallback, argsCallback);
//                    return;
//                }
            }
        }
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, name, callType);

        methodCompiler.loadThreadContext(); // [adapter, tc]

        // for visibility checking without requiring frame self
        // TODO: don't bother passing when fcall or vcall, and adjust callsite appropriately
        methodCompiler.loadSelf();
        
        if (receiverCallback != null) {
            receiverCallback.call(methodCompiler);
        } else {
            methodCompiler.loadSelf();
        }

        // super uses current block if none given
        if (callType == CallType.SUPER && closureArg == null) {
            closureArg = new CompilerCallback() {
                public void call(BodyCompiler context) {
                    methodCompiler.loadBlock();
                }
            };
        }
        
        String signature;
        String callSiteMethod = "call";
        // args
        if (argsCallback == null) {
            // block
            if (closureArg == null) {
                // no args, no block
                signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class));
            } else {
                // no args, with block
                if (iterator) callSiteMethod = "callIter";
                closureArg.call(methodCompiler);
                signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, Block.class));
            }
        } else {
            argsCallback.call(methodCompiler);
            // block
            if (closureArg == null) {
                // with args, no block
                switch (argsCallback.getArity()) {
                case 1:
                    signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class));
                    break;
                case 2:
                    signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class));
                    break;
                case 3:
                    signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class));
                    break;
                default:
                    signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject[].class));
                }
            } else {
                // with args, with block
                if (iterator) callSiteMethod = "callIter";
                closureArg.call(methodCompiler);
                
                switch (argsCallback.getArity()) {
                case 1:
                    signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, Block.class));
                    break;
                case 2:
                    signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, Block.class));
                    break;
                case 3:
                    signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, Block.class));
                    break;
                default:
                    signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject[].class, Block.class));
                }
            }
        }
        
        // adapter, tc, recv, args{0,1}, block{0,1}]

        method.invokevirtual(p(CallSite.class), callSiteMethod, signature);
    }
    
    public void invokeDynamicVarargs(String name, CompilerCallback receiverCallback, ArgumentsCallback argsCallback, CallType callType, CompilerCallback closureArg, boolean iterator) {
        assert argsCallback.getArity() == -1;
        
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, name, callType);

        methodCompiler.loadThreadContext(); // [adapter, tc]

        // for visibility checking without requiring frame self
        // TODO: don't bother passing when fcall or vcall, and adjust callsite appropriately
        methodCompiler.loadSelf();
        
        if (receiverCallback != null) {
            receiverCallback.call(methodCompiler);
        } else {
            methodCompiler.loadSelf();
        }

        // super uses current block if none given
        if (callType == CallType.SUPER && closureArg == null) {
            closureArg = new CompilerCallback() {
                public void call(BodyCompiler context) {
                    methodCompiler.loadBlock();
                }
            };
        }
        
        String signature;
        String callSiteMethod = "callVarargs";
        
        argsCallback.call(methodCompiler);
        
        // block
        if (closureArg == null) {
            signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject[].class));
        } else {
            if (iterator) callSiteMethod = "callVarargsIter";
            closureArg.call(methodCompiler);

            signature = sig(IRubyObject.class, params(ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject[].class, Block.class));
        }

        method.invokevirtual(p(CallSite.class), callSiteMethod, signature);
    }

    public void invokeDynamicSelfNoBlockZero(String name) {
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheMethod(methodCompiler, name);
        methodCompiler.loadThreadContext();
        methodCompiler.loadSelf();
        method.dup();
        method.invokeinterface(p(IRubyObject.class), "getMetaClass", sig(RubyClass.class));
        method.ldc(name);
        method.invokevirtual(p(DynamicMethod.class), "call", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, RubyModule.class, String.class));
    }

    public void invokeDynamicSelfNoBlockSpecificArity(String name, ArgumentsCallback argsCallback) {
        methodCompiler.loadThis();
        methodCompiler.loadThreadContext();
        methodCompiler.loadSelf();
        argsCallback.call(methodCompiler);
        String thisClass = methodCompiler.getScriptCompiler().getClassname();
        String signature1;
        switch (argsCallback.getArity()) {
        case 1:
            signature1 = sig(IRubyObject.class, "L" + thisClass + ";", ThreadContext.class, IRubyObject.class, IRubyObject.class);
            break;
        case 2:
            signature1 = sig(IRubyObject.class, "L" + thisClass + ";", ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class);
            break;
        case 3:
            signature1 = sig(IRubyObject.class, "L" + thisClass + ";", ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, IRubyObject.class);
            break;
        default:
            throw new RuntimeException("invalid arity for inline dyncall: " + argsCallback.getArity());
        }
        String synthMethodName = methodCompiler.getNativeMethodName() + "$call" + methodCompiler.getScriptCompiler().getAndIncrementMethodIndex();
        SkinnyMethodAdapter m2 = new SkinnyMethodAdapter(
                methodCompiler.getScriptCompiler().getClassVisitor(),
                Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC,
                synthMethodName,
                signature1,
                null,
                null);
        method.invokestatic(thisClass, synthMethodName, signature1);
        
        SkinnyMethodAdapter oldMethod = methodCompiler.method;
        methodCompiler.method = m2;
        m2.start();
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheMethod(methodCompiler, name);
        m2.aload(1); // ThreadContext
        m2.aload(2); // receiver
        m2.aload(2); // receiver
        m2.invokeinterface(p(IRubyObject.class), "getMetaClass", sig(RubyClass.class));
        m2.ldc(name);
        
        String signature2;
        switch (argsCallback.getArity()) {
        case 1:
            signature2 = sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, RubyModule.class, String.class, IRubyObject.class);
            m2.aload(3);
            break;
        case 2:
            signature2 = sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, RubyModule.class, String.class, IRubyObject.class, IRubyObject.class);
            m2.aload(3);
            m2.aload(4);
            break;
        case 3:
            signature2 = sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, RubyModule.class, String.class, IRubyObject.class, IRubyObject.class, IRubyObject.class);
            m2.aload(3);
            m2.aload(4);
            m2.aload(5);
            break;
        default:
            throw new RuntimeException("invalid arity for inline dyncall: " + argsCallback.getArity());
        }
        m2.invokevirtual(p(DynamicMethod.class), "call", signature2);
        m2.areturn();
        m2.end();
        methodCompiler.method = oldMethod;
    }

    public void invokeDynamicNoBlockZero(String name, CompilerCallback receiverCallback) {
        receiverCallback.call(methodCompiler);
        int recv = methodCompiler.getVariableCompiler().grabTempLocal();
        method.astore(recv);
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheMethod(methodCompiler, name, recv);
        methodCompiler.loadThreadContext();
        method.aload(recv);
        methodCompiler.getVariableCompiler().releaseTempLocal();
        method.dup();
        method.invokeinterface(p(IRubyObject.class), "getMetaClass", sig(RubyClass.class));
        method.ldc(name);
        method.invokevirtual(p(DynamicMethod.class), "call", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, RubyModule.class, String.class));
    }

    public void invokeDynamicNoBlockSpecificArity(String name, CompilerCallback receiverCallback, ArgumentsCallback argsCallback) {
        receiverCallback.call(methodCompiler);
        int recv = methodCompiler.getVariableCompiler().grabTempLocal();
        method.astore(recv);
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheMethod(methodCompiler, name, recv);
        methodCompiler.loadThreadContext();
        method.aload(recv);
        methodCompiler.getVariableCompiler().releaseTempLocal();
        method.dup();
        method.invokeinterface(p(IRubyObject.class), "getMetaClass", sig(RubyClass.class));
        method.ldc(name);
        argsCallback.call(methodCompiler);
        switch (argsCallback.getArity()) {
        case 1:
            method.invokevirtual(p(DynamicMethod.class), "call", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, RubyModule.class, String.class, IRubyObject.class));
            break;
        case 2:
            method.invokevirtual(p(DynamicMethod.class), "call", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, RubyModule.class, String.class, IRubyObject.class, IRubyObject.class));
            break;
        case 3:
            method.invokevirtual(p(DynamicMethod.class), "call", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, RubyModule.class, String.class, IRubyObject.class, IRubyObject.class, IRubyObject.class));
            break;
        }
    }

    public void invokeOpAsgnWithOr(String attrName, String attrAsgnName, CompilerCallback receiverCallback, ArgumentsCallback argsCallback) {
        receiverCallback.call(methodCompiler);
        method.dup();
        methodCompiler.loadThreadContext();
        methodCompiler.loadSelf();
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, attrName, CallType.FUNCTIONAL);
        
        methodCompiler.invokeUtilityMethod("preOpAsgnWithOrAnd", sig(IRubyObject.class, IRubyObject.class, ThreadContext.class, IRubyObject.class, CallSite.class));
        
        Label done = new Label();
        Label isTrue = new Label();
        
        method.dup();
        methodCompiler.invokeIRubyObject("isTrue", sig(boolean.class));
        method.ifne(isTrue);
        
        method.pop(); // pop extra attr value
        argsCallback.call(methodCompiler);
        methodCompiler.loadThreadContext();
        methodCompiler.loadSelf();
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, attrAsgnName, CallType.NORMAL);
        
        methodCompiler.invokeUtilityMethod("postOpAsgnWithOrAnd",
                sig(IRubyObject.class, IRubyObject.class, IRubyObject.class, ThreadContext.class, IRubyObject.class, CallSite.class));
        method.go_to(done);
        
        method.label(isTrue);
        method.swap();
        method.pop();
        
        method.label(done);
    }

    public void invokeOpAsgnWithAnd(String attrName, String attrAsgnName, CompilerCallback receiverCallback, ArgumentsCallback argsCallback) {
        receiverCallback.call(methodCompiler);
        method.dup();
        methodCompiler.loadThreadContext();
        methodCompiler.loadSelf();
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, attrName, CallType.FUNCTIONAL);
        
        methodCompiler.invokeUtilityMethod("preOpAsgnWithOrAnd", sig(IRubyObject.class, IRubyObject.class, ThreadContext.class, IRubyObject.class, CallSite.class));
        
        Label done = new Label();
        Label isFalse = new Label();
        
        method.dup();
        methodCompiler.invokeIRubyObject("isTrue", sig(boolean.class));
        method.ifeq(isFalse);
        
        method.pop(); // pop extra attr value
        argsCallback.call(methodCompiler);
        methodCompiler.loadThreadContext();
        methodCompiler.loadSelf();
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, attrAsgnName, CallType.NORMAL);
        
        methodCompiler.invokeUtilityMethod("postOpAsgnWithOrAnd",
                sig(IRubyObject.class, IRubyObject.class, IRubyObject.class, ThreadContext.class, IRubyObject.class, CallSite.class));
        method.go_to(done);
        
        method.label(isFalse);
        method.swap();
        method.pop();
        
        method.label(done);
    }

    public void invokeOpAsgnWithMethod(String operatorName, String attrName, String attrAsgnName, CompilerCallback receiverCallback, ArgumentsCallback argsCallback) {
        methodCompiler.loadThreadContext();
        methodCompiler.loadSelf();
        receiverCallback.call(methodCompiler);
        argsCallback.call(methodCompiler);
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, attrName, CallType.FUNCTIONAL);
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, operatorName, CallType.FUNCTIONAL);
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, attrAsgnName, CallType.NORMAL);
        
        methodCompiler.invokeUtilityMethod("opAsgnWithMethod",
                sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, CallSite.class, CallSite.class, CallSite.class));
    }

    public void invokeOpElementAsgnWithMethod(String operatorName, CompilerCallback receiverCallback, ArgumentsCallback argsCallback) {
        methodCompiler.loadThreadContext(); // [adapter, tc]
        methodCompiler.loadSelf();
        receiverCallback.call(methodCompiler);
        argsCallback.call(methodCompiler);
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, "[]", CallType.FUNCTIONAL);
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, operatorName, CallType.FUNCTIONAL);
        methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, "[]=", CallType.NORMAL);
        
        methodCompiler.invokeUtilityMethod("opElementAsgnWithMethod",
                sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class, CallSite.class, CallSite.class, CallSite.class));
    }

    public void yield(CompilerCallback argsCallback, boolean unwrap) {
        methodCompiler.loadBlock();
        methodCompiler.loadThreadContext();

        if (argsCallback != null) {
            argsCallback.call(methodCompiler);
        } else {
            method.aconst_null();
        }

        if (unwrap) {
            method.aconst_null();
            method.aconst_null();
            method.invokevirtual(p(Block.class), "yieldArray", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, RubyModule.class));
        } else {
            method.invokevirtual(p(Block.class), "yield", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class));
        }
    }

    public void yield19(CompilerCallback argsCallback, boolean unsplat) {
        methodCompiler.loadBlock();
        methodCompiler.loadThreadContext();

        if (argsCallback != null) {
            argsCallback.call(methodCompiler);
        } else {
            method.aconst_null();
        }

        if (unsplat) {
            methodCompiler.invokeUtilityMethod("unsplatValue19", sig(IRubyObject.class, IRubyObject.class));
        }

        method.aconst_null();
        method.aconst_null();
        method.invokevirtual(p(Block.class), "yieldArray", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, RubyModule.class));
    }

    public void yieldSpecific(ArgumentsCallback argsCallback) {
        methodCompiler.loadBlock();
        methodCompiler.loadThreadContext();

        String signature;
        if (argsCallback == null) {
            signature = sig(IRubyObject.class, ThreadContext.class);
        } else {
            argsCallback.call(methodCompiler);
            switch (argsCallback.getArity()) {
            case 1:
                signature = sig(IRubyObject.class, ThreadContext.class, IRubyObject.class);
                break;
            case 2:
                signature = sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class);
                break;
            case 3:
                signature = sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, IRubyObject.class, IRubyObject.class);
                break;
            default:
                throw new NotCompilableException("Can't do specific-arity call for > 3 args yet");
            }
        }

        method.invokevirtual(p(Block.class), "yieldSpecific", signature);
    }

    public void invokeEqq(ArgumentsCallback receivers, CompilerCallback argument) {
        if (argument == null) {
            receivers.call(methodCompiler);

            switch (receivers.getArity()) {
            case 1:
                methodCompiler.invokeUtilityMethod("invokeEqqForCaselessWhen", sig(boolean.class,
                        IRubyObject.class /*receiver*/
                        ));
                break;
            case 2:
                methodCompiler.invokeUtilityMethod("invokeEqqForCaselessWhen", sig(boolean.class,
                        IRubyObject.class, /*receiver*/
                        IRubyObject.class
                        ));
                break;
            case 3:
                methodCompiler.invokeUtilityMethod("invokeEqqForCaselessWhen", sig(boolean.class,
                        IRubyObject.class, /*receiver*/
                        IRubyObject.class,
                        IRubyObject.class
                        ));
                break;
            default:
                methodCompiler.invokeUtilityMethod("invokeEqqForCaselessWhen", sig(boolean.class,
                        IRubyObject[].class /*receiver*/
                        ));
            }
        } else {
            // arg and receiver already present on the stack
            methodCompiler.getScriptCompiler().getCacheCompiler().cacheCallSite(methodCompiler, "===", CallType.NORMAL);
            methodCompiler.loadThreadContext();
            methodCompiler.loadSelf();
            argument.call(methodCompiler);
            receivers.call(methodCompiler);

            switch (receivers.getArity()) {
            case 1:
                methodCompiler.invokeUtilityMethod("invokeEqqForCaseWhen", sig(boolean.class,
                        CallSite.class,
                        ThreadContext.class,
                        IRubyObject.class /*self*/,
                        IRubyObject.class, /*arg*/
                        IRubyObject.class /*receiver*/
                        ));
                break;
            case 2:
                methodCompiler.invokeUtilityMethod("invokeEqqForCaseWhen", sig(boolean.class,
                        CallSite.class,
                        ThreadContext.class,
                        IRubyObject.class /*self*/,
                        IRubyObject.class, /*arg*/
                        IRubyObject.class, /*receiver*/
                        IRubyObject.class
                        ));
                break;
            case 3:
                methodCompiler.invokeUtilityMethod("invokeEqqForCaseWhen", sig(boolean.class,
                        CallSite.class,
                        ThreadContext.class,
                        IRubyObject.class /*self*/,
                        IRubyObject.class, /*arg*/
                        IRubyObject.class, /*receiver*/
                        IRubyObject.class,
                        IRubyObject.class
                        ));
                break;
            default:
                methodCompiler.invokeUtilityMethod("invokeEqqForCaseWhen", sig(boolean.class,
                        CallSite.class,
                        ThreadContext.class,
                        IRubyObject.class /*self*/,
                        IRubyObject.class, /*arg*/
                        IRubyObject[].class /*receiver*/
                        ));
            }
        }
    }
}
