package test.javassist.bytecode.analysis;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.bytecode.AccessFlag;
import javassist.bytecode.BadBytecode;
import javassist.bytecode.Bytecode;
import javassist.bytecode.CodeIterator;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.Opcode;
import javassist.bytecode.analysis.Analyzer;
import javassist.bytecode.analysis.Frame;
import javassist.bytecode.analysis.Type;
import junit.framework.TestCase;

/**
 * Tests Analyzer
 *
 * @author Jason T. Greene
 */
public class AnalyzerTest extends TestCase {

    public void testCommonSupperArray() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.get(getClass().getName() + "$Dummy");
        CtMethod method = clazz.getDeclaredMethod("commonSuperArray");
        verifyArrayLoad(clazz, method, "java.lang.Number");
    }

    public  void testCommonInterfaceArray() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.get(getClass().getName() + "$Dummy");
        CtMethod method = clazz.getDeclaredMethod("commonInterfaceArray");
        verifyArrayLoad(clazz, method, "java.io.Serializable");
    }

    public  void testSharedInterfaceAndSuperClass() throws Exception {
        CtMethod method = ClassPool.getDefault().getMethod(
                getClass().getName() + "$Dummy", "sharedInterfaceAndSuperClass");
        verifyReturn(method, "java.io.Serializable");

        method = ClassPool.getDefault().getMethod(
                getClass().getName() + "$Dummy", "sharedOffsetInterfaceAndSuperClass");
        verifyReturn(method, "java.io.Serializable");

        method = ClassPool.getDefault().getMethod(
                getClass().getName() + "$Dummy", "sharedSuperWithSharedInterface");
        verifyReturn(method, getClass().getName() + "$Dummy$A");
    }

    public  void testArrayDifferentDims() throws Exception {
        CtMethod method = ClassPool.getDefault().getMethod(
                getClass().getName() + "$Dummy", "arrayDifferentDimensions1");
        verifyReturn(method, "java.lang.Cloneable[]");

        method = ClassPool.getDefault().getMethod(
                getClass().getName() + "$Dummy", "arrayDifferentDimensions2");
        verifyReturn(method, "java.lang.Object[][]");
    }

    public  void testReusedLocalMerge() throws Exception {
        CtMethod method = ClassPool.getDefault().getMethod(
                getClass().getName() + "$Dummy", "reusedLocalMerge");

        MethodInfo info = method.getMethodInfo2();
        Analyzer analyzer = new Analyzer();
        Frame[] frames = analyzer.analyze(method.getDeclaringClass(), info);
        assertNotNull(frames);
        int pos = findOpcode(info, Opcode.RETURN);
        Frame frame = frames[pos];
        assertEquals("java.lang.Object", frame.getLocal(2).getCtClass().getName());
    }

    private static int findOpcode(MethodInfo info, int opcode) throws BadBytecode {
        CodeIterator iter = info.getCodeAttribute().iterator();

        // find return
        int pos = 0;
        while (iter.hasNext()) {
            pos = iter.next();
            if (iter.byteAt(pos) == opcode)
                break;
        }
        return pos;
    }


    private static void verifyReturn(CtMethod method, String expected) throws BadBytecode {
        MethodInfo info = method.getMethodInfo2();
        CodeIterator iter = info.getCodeAttribute().iterator();

        // find areturn
        int pos = 0;
        while (iter.hasNext()) {
            pos = iter.next();
            if (iter.byteAt(pos) == Opcode.ARETURN)
                break;
        }

        Analyzer analyzer = new Analyzer();
        Frame[] frames = analyzer.analyze(method.getDeclaringClass(), info);
        assertNotNull(frames);
        Frame frame = frames[pos];
        assertEquals(expected, frame.peek().getCtClass().getName());
    }

    private static void verifyArrayLoad(CtClass clazz, CtMethod method, String component)
            throws BadBytecode {
        MethodInfo info = method.getMethodInfo2();
        CodeIterator iter = info.getCodeAttribute().iterator();

        // find aaload
        int pos = 0;
        while (iter.hasNext()) {
            pos = iter.next();
            if (iter.byteAt(pos) == Opcode.AALOAD)
                break;
        }

        Analyzer analyzer = new Analyzer();
        Frame[] frames = analyzer.analyze(clazz, info);
        assertNotNull(frames);
        Frame frame = frames[pos];
        assertNotNull(frame);

        Type type = frame.getStack(frame.getTopIndex() - 1);
        assertEquals(component + "[]", type.getCtClass().getName());

        pos = iter.next();
        frame = frames[pos];
        assertNotNull(frame);

        type = frame.getStack(frame.getTopIndex());
        assertEquals(component, type.getCtClass().getName());
    }

    private static void addJump(Bytecode code, int opcode, int pos) {
        int current = code.currentPc();
        code.addOpcode(opcode);
        code.addIndex(pos - current);
    }

    public void testDeadCode() throws Exception {
        CtMethod method = generateDeadCode(ClassPool.getDefault());
        Analyzer analyzer = new Analyzer();
        Frame[] frames = analyzer.analyze(method.getDeclaringClass(), method.getMethodInfo2());
        assertNotNull(frames);
        assertNull(frames[4]);
        assertNotNull(frames[5]);
        verifyReturn(method, "java.lang.String");
    }

    public void testInvalidCode() throws Exception {
        CtMethod method = generateInvalidCode(ClassPool.getDefault());
        Analyzer analyzer = new Analyzer();
        try {
            analyzer.analyze(method.getDeclaringClass(), method.getMethodInfo2());
        } catch (BadBytecode e) {
            return;
        }

        fail("Invalid code should have triggered a BadBytecode exception");
    }

    public void testCodeFalloff() throws Exception {
        CtMethod method = generateCodeFalloff(ClassPool.getDefault());
        Analyzer analyzer = new Analyzer();
        try {
            analyzer.analyze(method.getDeclaringClass(), method.getMethodInfo2());
        } catch (BadBytecode e) {
            return;
        }

        fail("Code falloff should have triggered a BadBytecode exception");
    }

    public void testJsrMerge() throws Exception {
        CtMethod method = generateJsrMerge(ClassPool.getDefault());
        Analyzer analyzer = new Analyzer();
        analyzer.analyze(method.getDeclaringClass(), method.getMethodInfo2());
        verifyReturn(method, "java.lang.String");
    }

    public void testJsrMerge2() throws Exception {
        CtMethod method = generateJsrMerge2(ClassPool.getDefault());
        Analyzer analyzer = new Analyzer();
        analyzer.analyze(method.getDeclaringClass(), method.getMethodInfo2());
        verifyReturn(method, "java.lang.String");
    }

    private CtMethod generateDeadCode(ClassPool pool) throws Exception {
        CtClass clazz = pool.makeClass(getClass().getName() + "$Generated0");
        CtClass stringClass = pool.get("java.lang.String");
        CtMethod method = new CtMethod(stringClass, "foo", new CtClass[0], clazz);
        MethodInfo info = method.getMethodInfo2();
        info.setAccessFlags(AccessFlag.PUBLIC | AccessFlag.STATIC);
        Bytecode code = new Bytecode(info.getConstPool(), 1, 2);
        /* 0 */ code.addIconst(1);
        /* 1 */ addJump(code, Opcode.GOTO, 5);
        /* 4 */ code.addIconst(0); // DEAD
        /* 5 */ code.addIconst(1);
        /* 6 */ code.addInvokestatic(stringClass, "valueOf", stringClass, new CtClass[]{CtClass.intType});
        /* 9 */ code.addOpcode(Opcode.ARETURN);
        info.setCodeAttribute(code.toCodeAttribute());
        clazz.addMethod(method);

        return method;
    }

    private CtMethod generateInvalidCode(ClassPool pool) throws Exception {
        CtClass clazz = pool.makeClass(getClass().getName() + "$Generated4");
        CtClass intClass = pool.get("java.lang.Integer");
        CtClass stringClass = pool.get("java.lang.String");
        CtMethod method = new CtMethod(stringClass, "foo", new CtClass[0], clazz);
        MethodInfo info = method.getMethodInfo2();
        info.setAccessFlags(AccessFlag.PUBLIC | AccessFlag.STATIC);
        Bytecode code = new Bytecode(info.getConstPool(), 1, 2);
        /* 0 */ code.addIconst(1);
        /* 1 */ code.addInvokestatic(intClass, "valueOf", intClass, new CtClass[]{CtClass.intType});
        /* 4 */ code.addOpcode(Opcode.ARETURN);
        info.setCodeAttribute(code.toCodeAttribute());
        clazz.addMethod(method);

        return method;
    }


    private CtMethod generateCodeFalloff(ClassPool pool) throws Exception {
        CtClass clazz = pool.makeClass(getClass().getName() + "$Generated3");
        CtClass stringClass = pool.get("java.lang.String");
        CtMethod method = new CtMethod(stringClass, "foo", new CtClass[0], clazz);
        MethodInfo info = method.getMethodInfo2();
        info.setAccessFlags(AccessFlag.PUBLIC | AccessFlag.STATIC);
        Bytecode code = new Bytecode(info.getConstPool(), 1, 2);
        /* 0 */ code.addIconst(1);
        /* 1 */ code.addInvokestatic(stringClass, "valueOf", stringClass, new CtClass[]{CtClass.intType});
        info.setCodeAttribute(code.toCodeAttribute());
        clazz.addMethod(method);

        return method;
    }

    private CtMethod generateJsrMerge(ClassPool pool) throws Exception {
        CtClass clazz = pool.makeClass(getClass().getName() + "$Generated1");
        CtClass stringClass = pool.get("java.lang.String");
        CtMethod method = new CtMethod(stringClass, "foo", new CtClass[0], clazz);
        MethodInfo info = method.getMethodInfo2();
        info.setAccessFlags(AccessFlag.PUBLIC | AccessFlag.STATIC);
        Bytecode code = new Bytecode(info.getConstPool(), 1, 2);
        /* 0 */ code.addIconst(5);
        /* 1 */ code.addIstore(0);
        /* 2 */ addJump(code, Opcode.JSR, 7);
        /* 5 */ code.addAload(0);
        /* 6 */ code.addOpcode(Opcode.ARETURN);
        /* 7 */ code.addAstore(1);
        /* 8 */ code.addIconst(3);
        /* 9 */ code.addInvokestatic(stringClass, "valueOf", stringClass, new CtClass[]{CtClass.intType});
        /* 12 */ code.addAstore(0);
        /* 12 */ code.addRet(1);
        info.setCodeAttribute(code.toCodeAttribute());
        clazz.addMethod(method);
        //System.out.println(clazz.toClass().getMethod("foo", new Class[0]).invoke(null, new Object[0]));

        return method;
    }

    private CtMethod generateJsrMerge2(ClassPool pool) throws Exception {
        CtClass clazz = pool.makeClass(getClass().getName() + "$Generated2");
        CtClass stringClass = pool.get("java.lang.String");
        CtMethod method = new CtMethod(stringClass, "foo", new CtClass[0], clazz);
        MethodInfo info = method.getMethodInfo2();
        info.setAccessFlags(AccessFlag.PUBLIC | AccessFlag.STATIC);
        Bytecode code = new Bytecode(info.getConstPool(), 1, 2);
        /* 0 */ addJump(code, Opcode.JSR, 5);
        /* 3 */ code.addAload(0);
        /* 4 */ code.addOpcode(Opcode.ARETURN);
        /* 5 */ code.addAstore(1);
        /* 6 */ code.addIconst(4);
        /* 7 */ code.addInvokestatic(stringClass, "valueOf", stringClass, new CtClass[]{CtClass.intType});
        /* 10 */ code.addAstore(0);
        /* 11 */ code.addRet(1);
        info.setCodeAttribute(code.toCodeAttribute());
        clazz.addMethod(method);

        return method;
    }

    public static class Dummy {
        public Serializable commonSuperArray(int x) {
            Number[] n;

            if (x > 5) {
                n = new Long[10];
            } else {
                n = new Double[5];
            }

            return n[x];
        }

        public Serializable commonInterfaceArray(int x) {
            Serializable[] n;

            if (x > 5) {
                n = new Long[10];
            } else if (x > 3) {
                n = new Double[5];
            } else {
                n = new String[3];
            }

            return n[x];
        }


        public static class A {};
        public static class B1 extends A implements Serializable {
            private static final long serialVersionUID = 1L;};
        public static class B2 extends A implements Serializable {
            private static final long serialVersionUID = 1L;};
        public static class A2 implements Serializable, Cloneable {
            private static final long serialVersionUID = 1L;};
        public static class A3 implements Serializable, Cloneable {
            private static final long serialVersionUID = 1L;};
        public static class B3 extends A {};
        public static class C31 extends B3 implements Serializable {
            private static final long serialVersionUID = 1L;};


        public void dummy(Serializable s) {}

        public Object sharedInterfaceAndSuperClass(int x) {
            Serializable s;

            if (x > 5) {
                s = new B1();
            } else {
                s = new B2();
            }

            dummy(s);

            return s;
        }

        public A sharedSuperWithSharedInterface(int x) {
            A a;

            if (x > 5) {
                a = new B1();
            } else if (x > 3) {
                a = new B2();
            } else {
                a = new C31();
            }

            return a;
        }


        @SuppressWarnings("rawtypes")
        public void reusedLocalMerge() {
             ArrayList list = new ArrayList();
             try {
               Iterator i = list.iterator();
               i.hasNext();
             } catch (Exception e) {
             }
        }

        public Object sharedOffsetInterfaceAndSuperClass(int x) {
            Serializable s;

            if (x > 5) {
                s = new B1();
            } else {
                s = new C31();
            }

            dummy(s);

            return s;
        }


        public Object arrayDifferentDimensions1(int x) {
            Object[] n;

            if ( x > 5) {
                n = new Number[1][1];
            } else {
                n = new Cloneable[1];
            }


            return n;
        }

        public Object arrayDifferentDimensions2(int x) {
            Object[] n;

            if ( x> 5) {
                n = new String[1][1];
            } else {
                n = new Number[1][1][1][1];
            }

            return n;
        }
    }
}
