/**************************************************************************************
 * Copyright (c) Jonas Bonér, Alexandre Vasseur. All rights reserved.                 *
 * http://aspectwerkz.codehaus.org                                                    *
 * ---------------------------------------------------------------------------------- *
 * The software in this package is published under the terms 8of the LGPL license      *
 * a copy of which has been included with this distribution in the license.txt file.  *
 **************************************************************************************/
package org.codehaus.aspectwerkz.transform.inlining;

import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;

import org.codehaus.aspectwerkz.definition.SystemDefinition;
import org.codehaus.aspectwerkz.expression.ExpressionContext;
import org.codehaus.aspectwerkz.expression.PointcutType;
import org.codehaus.aspectwerkz.reflect.ClassInfo;
import org.codehaus.aspectwerkz.reflect.impl.asm.AsmClassInfo;
import org.codehaus.aspectwerkz.transform.AspectWerkzPreProcessor;
import org.codehaus.aspectwerkz.transform.Context;
import org.codehaus.aspectwerkz.transform.WeavingStrategy;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.attrs.Attributes;
                                        
/**
 * A weaving strategy implementing a weaving scheme based on inlining.
 *
 * @author <a href="mailto:jboner@codehaus.org">Jonas Bonér </a>
 * @author <a href="mailto:alex@gnilux.com">Alexandre Vasseur </a>
 */
public class InliningWeavingStrategy implements WeavingStrategy {

    /**
     * Initializes the weaving strategy.
     *
     * @param params
     */
    public void initialize(final Hashtable params) {
    }

    /**
     * Performs the weaving.
     *
     * @param className
     * @param context
     */
    public void transform(String className, final Context context) {
        try {
            if (className.endsWith(JoinPointCompiler.JOIN_POINT_CLASS_SUFFIX)
                    || className.endsWith(JoinPointCompiler.JOIN_POINT_BASE_CLASS_SUFFIX)) {
                return;
            }

            byte[] bytecode = context.getInitialBytecode();
            ClassLoader loader = context.getLoader();

            ClassInfo classInfo = AsmClassInfo.getClassInfo(bytecode, loader);

            List definitions = context.getDefinitions();
            ExpressionContext[] ctxs = new ExpressionContext[]{
                new ExpressionContext(PointcutType.EXECUTION, classInfo, classInfo),
                new ExpressionContext(PointcutType.CALL, null, classInfo),
                new ExpressionContext(PointcutType.GET, classInfo, classInfo),
                new ExpressionContext(PointcutType.SET, classInfo, classInfo),
                new ExpressionContext(PointcutType.HANDLER, null, classInfo),
                new ExpressionContext(PointcutType.STATIC_INITIALIZATION, classInfo, classInfo),
                new ExpressionContext(PointcutType.WITHIN, classInfo, classInfo)
            };
            if (classFilter(definitions, ctxs, classInfo)) {
                return;
            }

            System.out.println("AW INFO: weaving class = " + className + " in class loader " + loader);

            // build the ClassInfo from the bytecode to avoid loading it from the loader resource stream later
            // to support stub weaving
            AsmClassInfo.getClassInfo(bytecode, loader);

            final ClassReader cr = new ClassReader(bytecode);
            final ClassWriter cw = AsmHelper.newClassWriter(true);

            // TODO should we provide some option to do JITgen at weave time instead of load time ?
            //            Class joinPointBase = JoinPointCompiler.compileJoinPointBase(className, loader);

            // register the Adapter from last to first
            // TODO could we avoid object creation
            ClassVisitor first = cw;
            first = new JoinPointInitClassAdapter(first, context);
            first = new WrapperMethodClassAdapter(first, classInfo, context);
            first = new MethodExecutionClassAdapter(first, loader, classInfo, context);
            first = new ConstructorBodyAdapter(first, loader, classInfo, context);
            first = new MethodCallClassAdapter(first, loader, classInfo, context);
            first = new FieldSetGetClassAdapter(first, loader, classInfo, context);
            cr.accept(first, Attributes.getDefaultAttributes(),false);

            // FIXME add a last FinalizingClassAdapter that adds the serialveruid etc.
            //cr.accept(new FinalizingClassAdapter(cw, loader, classInfo, context), false);

            // TODO: INNER CLASS OR NOT?
            // loop over emitted jp and flag them as inner classes
//            for (Iterator iterator = ((ContextImpl) context).getEmittedInlinedJoinPoint().iterator(); iterator.hasNext();) {
//                String joinPointClassName = ((ContextImpl.EmittedInlinedJoinPoint) iterator.next()).joinPointClassName;
//                int innerIndex = joinPointClassName.lastIndexOf('$');
//                cw.visitInnerClass(joinPointClassName,
//                        joinPointClassName.substring(0, innerIndex),
//                        joinPointClassName.substring(innerIndex + 1, joinPointClassName.length()),
//                        Constants.ACC_PUBLIC + Constants.ACC_STATIC);
//            }


            context.setCurrentBytecode(cw.toByteArray());

            // FIXME: remove when in release time or in debugging trouble (;-) - Alex)
            // FAKE multiweaving - which is a requirement
            //            Object multi = context.getMetaData("FAKE");
            //            if (multi == null) {
            //                context.addMetaData("FAKE", "FAKE");
            //                transform(className, context);
            //            }

            // TODO remove dump force before release
            AspectWerkzPreProcessor.dumpForce(context);

        } catch (Throwable t) {
            // TODO handle exception
            System.err.println("possible to handle exception? : " + t.toString());
            t.printStackTrace();
        }
    }

    /**
     * Creates a new transformation context.
     *
     * @param name
     * @param bytecode
     * @param loader
     * @return
     */
    public Context newContext(final String name, final byte[] bytecode, final ClassLoader loader) {
        return new ContextImpl(name, bytecode, loader);
    }

    /**
     * Filters out the classes that are not eligible for transformation.
     *
     * @param definitions the definitions
     * @param ctxs        an array with the contexts
     * @param classInfo   the class to filter
     * @return boolean true if the class should be filtered out
     */
    public static boolean classFilter(final List definitions, final ExpressionContext[] ctxs, final ClassInfo classInfo) {
        if (classInfo.isInterface()) {
            return true;
        }
        for (Iterator defs = definitions.iterator(); defs.hasNext();) {
            if (classFilter((SystemDefinition) defs.next(), ctxs, classInfo)) {
                continue;
            } else {
                return false;
            }
        }
        return true;
    }

    /**
     * Filters out the classes that are not eligible for transformation.
     *
     * @param definition the definition
     * @param ctxs       an array with the contexts
     * @param classInfo  the class to filter
     * @return boolean true if the class should be filtered out
     * @TODO: when a class had execution pointcut that were removed it must be unweaved, thus not filtered out How to
     * handle that? cache lookup? or custom class level attribute ?
     */
    public static boolean classFilter(final SystemDefinition definition,
                                      final ExpressionContext[] ctxs,
                                      final ClassInfo classInfo) {
        if (classInfo.isInterface()) {
            return true;
        }
        String className = classInfo.getName();
        if (definition.inExcludePackage(className)) {
            return true;
        }
        if (!definition.inIncludePackage(className)) {
            return true;
        }
        if (definition.isAdvised(ctxs)) {
            return false;
        }
        if (definition.inPreparePackage(className)) {
            return false;
        }
        return true;
    }
}