package sample.evolve;

import javassist.*;

/**
 * Evolution provides a set of methods for instrumenting bytecodes.
 * 
 * For class evolution, updatable class A is renamed to B. Then an abstract
 * class named A is produced as the super class of B. If the original class A
 * has a public method m(), then the abstract class A has an abstract method
 * m().
 * 
 * abstract class A abstract m() _makeInstance() | class A --------> class B m()
 * m()
 * 
 * Also, all the other classes are translated so that "new A(i)" in the methods
 * is replaced with "_makeInstance(i)". This makes it possible to change the
 * behavior of the instantiation of the class A.
 */
public class Evolution implements Translator {
    public final static String handlerMethod = "_makeInstance";

    public final static String latestVersionField = VersionManager.latestVersionField;

    public final static String versionManagerMethod = "initialVersion";

    private static CtMethod trapMethod;

    private static final int initialVersion = 0;

    private ClassPool pool;

    private String updatableClassName = null;

    private CtClass updatableClass = null;

    public void start(ClassPool _pool) throws NotFoundException {
        pool = _pool;

        // Get the definition of Sample.make() and store it into trapMethod
        // for later use.
        trapMethod = _pool.getMethod("sample.evolve.Sample", "make");
    }

    public void onLoad(ClassPool _pool, String classname)
            throws NotFoundException, CannotCompileException {
        onLoadUpdatable(classname);

        /*
         * Replaces all the occurrences of the new operator with a call to
         * _makeInstance().
         */
        CtClass clazz = _pool.get(classname);
        CtClass absClass = updatableClass;
        CodeConverter converter = new CodeConverter();
        converter.replaceNew(absClass, absClass, handlerMethod);
        clazz.instrument(converter);
    }

    private void onLoadUpdatable(String classname) throws NotFoundException,
            CannotCompileException {
        // if the class is a concrete class,
        // classname is <updatableClassName>$$<version>.

        int i = classname.lastIndexOf("$$");
        if (i <= 0)
            return;

        String orgname = classname.substring(0, i);
        if (!orgname.equals(updatableClassName))
            return;

        int version;
        try {
            version = Integer.parseInt(classname.substring(i + 2));
        }
        catch (NumberFormatException e) {
            throw new NotFoundException(classname, e);
        }

        CtClass clazz = pool.getAndRename(orgname, classname);
        makeConcreteClass(clazz, updatableClass, version);
    }

    /*
     * Register an updatable class.
     */
    public void makeUpdatable(String classname) throws NotFoundException,
            CannotCompileException {
        if (pool == null)
            throw new RuntimeException(
                    "Evolution has not been linked to ClassPool.");

        CtClass c = pool.get(classname);
        updatableClassName = classname;
        updatableClass = makeAbstractClass(c);
    }

    /**
     * Produces an abstract class.
     */
    protected CtClass makeAbstractClass(CtClass clazz)
            throws CannotCompileException, NotFoundException {
        int i;

        CtClass absClass = pool.makeClass(clazz.getName());
        absClass.setModifiers(Modifier.PUBLIC | Modifier.ABSTRACT);
        absClass.setSuperclass(clazz.getSuperclass());
        absClass.setInterfaces(clazz.getInterfaces());

        // absClass.inheritAllConstructors();

        CtField fld = new CtField(pool.get("java.lang.Class"),
                latestVersionField, absClass);
        fld.setModifiers(Modifier.PUBLIC | Modifier.STATIC);

        CtField.Initializer finit = CtField.Initializer.byCall(pool
                .get("sample.evolve.VersionManager"), versionManagerMethod,
                new String[] { clazz.getName() });
        absClass.addField(fld, finit);

        CtField[] fs = clazz.getDeclaredFields();
        for (i = 0; i < fs.length; ++i) {
            CtField f = fs[i];
            if (Modifier.isPublic(f.getModifiers()))
                absClass.addField(new CtField(f.getType(), f.getName(),
                        absClass));
        }

        CtConstructor[] cs = clazz.getDeclaredConstructors();
        for (i = 0; i < cs.length; ++i) {
            CtConstructor c = cs[i];
            int mod = c.getModifiers();
            if (Modifier.isPublic(mod)) {
                CtMethod wm = CtNewMethod.wrapped(absClass, handlerMethod, c
                        .getParameterTypes(), c.getExceptionTypes(),
                        trapMethod, null, absClass);
                wm.setModifiers(Modifier.PUBLIC | Modifier.STATIC);
                absClass.addMethod(wm);
            }
        }

        CtMethod[] ms = clazz.getDeclaredMethods();
        for (i = 0; i < ms.length; ++i) {
            CtMethod m = ms[i];
            int mod = m.getModifiers();
            if (Modifier.isPublic(mod))
                if (Modifier.isStatic(mod))
                    throw new CannotCompileException(
                            "static methods are not supported.");
                else {
                    CtMethod m2 = CtNewMethod.abstractMethod(m.getReturnType(),
                            m.getName(), m.getParameterTypes(), m
                                    .getExceptionTypes(), absClass);
                    absClass.addMethod(m2);
                }
        }

        return absClass;
    }

    /**
     * Modifies the given class file so that it is a subclass of the abstract
     * class produced by makeAbstractClass().
     * 
     * Note: the naming convention must be consistent with
     * VersionManager.update().
     */
    protected void makeConcreteClass(CtClass clazz, CtClass abstractClass,
            int version) throws CannotCompileException, NotFoundException {
        int i;
        clazz.setSuperclass(abstractClass);
        CodeConverter converter = new CodeConverter();
        CtField[] fs = clazz.getDeclaredFields();
        for (i = 0; i < fs.length; ++i) {
            CtField f = fs[i];
            if (Modifier.isPublic(f.getModifiers()))
                converter.redirectFieldAccess(f, abstractClass, f.getName());
        }

        CtConstructor[] cs = clazz.getDeclaredConstructors();
        for (i = 0; i < cs.length; ++i)
            cs[i].instrument(converter);

        CtMethod[] ms = clazz.getDeclaredMethods();
        for (i = 0; i < ms.length; ++i)
            ms[i].instrument(converter);
    }
}
