/*
 * Copyright 2004 Chris Nokleberg
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.sixlegs.async;

import java.util.Iterator;
import net.sf.cglib.core.*;
import net.sf.cglib.transform.ClassEmitterTransformer;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.Type;
import org.objectweb.asm.attrs.Annotation;
import org.objectweb.asm.attrs.RuntimeInvisibleAnnotations;

public class AsyncTransformer
extends ClassEmitterTransformer
{
    private static final Type ASYNC = TypeUtils.parseType("com.sixlegs.async.Async");
    private static final Type ASYNC_THREAD = TypeUtils.parseType("com.sixlegs.async.AsyncThread");
    private static final Type THREAD = TypeUtils.parseType("java.lang.Thread");
    private static final Type METHOD = TypeUtils.parseType("java.lang.reflect.Method");
    private static final Signature THREAD_CSTRUCT = TypeUtils.parseConstructor("java.lang.reflect.Method, Object, Object[]");
    private static final Signature THREAD_START = TypeUtils.parseSignature("void start()");
    
    private int counter = 0;
    
    synchronized private String nextName(String name) {
        return name + "$$AsyncByCglib$$" + counter++;
    }

    public CodeEmitter begin_method(int access, Signature sig, Type[] exceptions, Attribute attrs) {

        // loop over all the attributes (if any), looking for @Async
        boolean async = false;
        while (attrs != null) {
            if (attrs instanceof RuntimeInvisibleAnnotations) {
                for (Iterator it = ((RuntimeInvisibleAnnotations)attrs).annotations.iterator(); it.hasNext();) {
                    Annotation ann = (Annotation)it.next();
                    if (ann.type.equals(ASYNC.getDescriptor())) {
                        async = true;
                        break;
                    }
                }
            }
            attrs = attrs.next;
        }

        CodeEmitter stub = super.begin_method(access, sig, exceptions, attrs);

        // if we didn't find an attribute, emit method unaltered
        if (!async)
            return stub;

        // not sure if there is a way to check this at compile-time
        if (!Type.VOID_TYPE.equals(sig.getReturnType()))
            throw new IllegalArgumentException("Async attribute only allowed on methods returning void");

        String renamed = nextName(sig.getName());
        CodeEmitter real = super.begin_method(access,
                                              new Signature(renamed, sig.getDescriptor()),
                                              exceptions,
                                              null);

        // load the java.lang.reflect.Method object for the renamed method into a static field
        declare_field(Constants.PRIVATE_FINAL_STATIC, renamed, METHOD, null, null);
        CodeEmitter hook = getStaticHook();
        EmitUtils.load_method(hook, real.getMethodInfo());
        hook.putfield(renamed);

        // create a new AsyncThread and pass in Method and arguments to constructor
        stub.new_instance(ASYNC_THREAD);
        stub.dup();
        stub.getfield(renamed);
        if (TypeUtils.isStatic(access)) {
            // pass null for "this" if the method is static
            stub.aconst_null();
        } else {
            stub.load_this();
        }
        stub.create_arg_array();
        stub.invoke_constructor(ASYNC_THREAD, THREAD_CSTRUCT);

        // start the thread
        stub.invoke_virtual(THREAD, THREAD_START);
        stub.return_value();
        stub.end_method();

        // return the renamed method, where the original method bytecode will be sent
        return real;
    }
}
