/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * 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.android.tools.layoutlib.create;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodAdapter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.signature.SignatureReader;
import org.objectweb.asm.signature.SignatureVisitor;
import org.objectweb.asm.signature.SignatureWriter;

/**
 * This class visitor renames a class from a given old name to a given new name.
 * The class visitor will also rename all inner classes and references in the methods.
 * <p/>
 * 
 * For inner classes, this handles only the case where the outer class name changes.
 * The inner class name should remain the same. 
 */
public class RenameClassAdapter extends ClassAdapter {

    
    private final String mOldName;
    private final String mNewName;
    private String mOldBase;
    private String mNewBase;

    /**
     * Creates a class visitor that renames a class from a given old name to a given new name.
     * The class visitor will also rename all inner classes and references in the methods.
     * The names must be full qualified internal ASM names (e.g. com/blah/MyClass$InnerClass).
     */
    public RenameClassAdapter(ClassWriter cv, String oldName, String newName) {
        super(cv);
        mOldBase = mOldName = oldName;
        mNewBase = mNewName = newName;
        
        int pos = mOldName.indexOf('$');
        if (pos > 0) {
            mOldBase = mOldName.substring(0, pos);
        }
        pos = mNewName.indexOf('$');
        if (pos > 0) {
            mNewBase = mNewName.substring(0, pos);
        }
        
        assert (mOldBase == null && mNewBase == null) || (mOldBase != null && mNewBase != null);
    }


    /**
     * Renames a type descriptor, e.g. "Lcom.package.MyClass;"
     * If the type doesn't need to be renamed, returns the input string as-is.
     */
    String renameTypeDesc(String desc) {
        if (desc == null) {
            return null;
        }

        return renameType(Type.getType(desc));
    }
    
    /**
     * Renames an object type, e.g. "Lcom.package.MyClass;" or an array type that has an
     * object element, e.g. "[Lcom.package.MyClass;"
     * If the type doesn't need to be renamed, returns the internal name of the input type.
     */
    String renameType(Type type) {
        if (type == null) {
            return null;
        }

        if (type.getSort() == Type.OBJECT) {
            String in = type.getInternalName();
            return "L" + renameInternalType(in) + ";";
        } else if (type.getSort() == Type.ARRAY) {
            StringBuilder sb = new StringBuilder();
            for (int n = type.getDimensions(); n > 0; n--) {
                sb.append('[');
            }
            sb.append(renameType(type.getElementType()));
            return sb.toString();
        }
        return type.getDescriptor();
    }

    /**
     * Renames an object type, e.g. "Lcom.package.MyClass;" or an array type that has an
     * object element, e.g. "[Lcom.package.MyClass;".
     * This is like renameType() except that it returns a Type object.
     * If the type doesn't need to be renamed, returns the input type object.
     */
    Type renameTypeAsType(Type type) {
        if (type == null) {
            return null;
        }

        if (type.getSort() == Type.OBJECT) {
            String in = type.getInternalName();
            String newIn = renameInternalType(in);
            if (newIn != in) {
                return Type.getType("L" + newIn + ";");
            }
        } else if (type.getSort() == Type.ARRAY) {
            StringBuilder sb = new StringBuilder();
            for (int n = type.getDimensions(); n > 0; n--) {
                sb.append('[');
            }
            sb.append(renameType(type.getElementType()));
            return Type.getType(sb.toString());
        }
        return type;
    }

    /**
     * Renames an internal type name, e.g. "com.package.MyClass".
     * If the type doesn't need to be renamed, returns the input string as-is.
     * <p/>
     * The internal type of some of the MethodVisitor turns out to be a type
       descriptor sometimes so descriptors are renamed too.
     */
    String renameInternalType(String type) {
        if (type == null) {
            return null;
        }

        if (type.equals(mOldName)) {
            return mNewName;
        }

        if (mOldBase != mOldName && type.equals(mOldBase)) {
            return mNewBase;
        }
    
        int pos = type.indexOf('$');
        if (pos == mOldBase.length() && type.startsWith(mOldBase)) {
            return mNewBase + type.substring(pos);
        }

        // The internal type of some of the MethodVisitor turns out to be a type
        // descriptor sometimes. This is the case with visitTypeInsn(type) and
        // visitMethodInsn(owner). We try to detect it and adjust it here.
        if (type.indexOf(';') > 0) {
            type = renameTypeDesc(type);
        }

        return type;
    }

    /**
     * Renames a method descriptor, i.e. applies renameType to all arguments and to the
     * return value.
     */
    String renameMethodDesc(String desc) {
        if (desc == null) {
            return null;
        }

        Type[] args = Type.getArgumentTypes(desc);

        StringBuilder sb = new StringBuilder("(");
        for (Type arg : args) {
            String name = renameType(arg);
            sb.append(name);
        }
        sb.append(')');
        
        Type ret = Type.getReturnType(desc);
        String name = renameType(ret);
        sb.append(name);

        return sb.toString();
    }

    
    /**
     * Renames the ClassSignature handled by ClassVisitor.visit 
     * or the MethodTypeSignature handled by ClassVisitor.visitMethod.
     */
    String renameTypeSignature(String sig) {
        if (sig == null) {
            return null;
        }
        SignatureReader reader = new SignatureReader(sig);
        SignatureWriter writer = new SignatureWriter();
        reader.accept(new RenameSignatureAdapter(writer));
        sig = writer.toString();
        return sig;
    }

    
    /**
     * Renames the FieldTypeSignature handled by ClassVisitor.visitField
     * or MethodVisitor.visitLocalVariable.
     */
    String renameFieldSignature(String sig) {
        if (sig == null) {
            return null;
        }
        SignatureReader reader = new SignatureReader(sig);
        SignatureWriter writer = new SignatureWriter();
        reader.acceptType(new RenameSignatureAdapter(writer));
        sig = writer.toString();
        return sig;
    }

    
    //----------------------------------
    // Methods from the ClassAdapter
    
    @Override
    public void visit(int version, int access, String name, String signature,
            String superName, String[] interfaces) {
        name = renameInternalType(name);
        superName = renameInternalType(superName);
        signature = renameTypeSignature(signature);
        
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public void visitInnerClass(String name, String outerName, String innerName, int access) {
        assert outerName.equals(mOldName);
        outerName = renameInternalType(outerName);
        name = outerName + "$" + innerName;
        super.visitInnerClass(name, outerName, innerName, access);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
            String signature, String[] exceptions) {
        desc = renameMethodDesc(desc);
        signature = renameTypeSignature(signature);
        MethodVisitor mw = super.visitMethod(access, name, desc, signature, exceptions);
        return new RenameMethodAdapter(mw);
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        desc = renameTypeDesc(desc);
        return super.visitAnnotation(desc, visible);
    }
    
    @Override
    public FieldVisitor visitField(int access, String name, String desc,
            String signature, Object value) {
        desc = renameTypeDesc(desc);
        signature = renameFieldSignature(signature);
        return super.visitField(access, name, desc, signature, value);
    }
    
    
    //----------------------------------

    /**
     * A method visitor that renames all references from an old class name to a new class name.
     */
    public class RenameMethodAdapter extends MethodAdapter {

        /**
         * Creates a method visitor that renames all references from a given old name to a given new
         * name. The method visitor will also rename all inner classes.
         * The names must be full qualified internal ASM names (e.g. com/blah/MyClass$InnerClass).
         */
        public RenameMethodAdapter(MethodVisitor mv) {
            super(mv);
        }

        @Override
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            desc = renameTypeDesc(desc);
            
            return super.visitAnnotation(desc, visible);
        }

        @Override
        public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
            desc = renameTypeDesc(desc);

            return super.visitParameterAnnotation(parameter, desc, visible);
        }

        @Override
        public void visitTypeInsn(int opcode, String type) {
            type = renameInternalType(type);
            
            super.visitTypeInsn(opcode, type);
        }

        @Override
        public void visitFieldInsn(int opcode, String owner, String name, String desc) {
            owner = renameInternalType(owner);
            desc = renameTypeDesc(desc);

            super.visitFieldInsn(opcode, owner, name, desc);
        }

        @Override
        public void visitMethodInsn(int opcode, String owner, String name, String desc) {
            owner = renameInternalType(owner);
            desc = renameMethodDesc(desc);

            super.visitMethodInsn(opcode, owner, name, desc);
        }
        
        @Override
        public void visitLdcInsn(Object cst) {
            // If cst is a Type, this means the code is trying to pull the .class constant
            // for this class, so it needs to be renamed too.
            if (cst instanceof Type) {
                cst = renameTypeAsType((Type) cst);
            }
            super.visitLdcInsn(cst);
        }

        @Override
        public void visitMultiANewArrayInsn(String desc, int dims) {
            desc = renameTypeDesc(desc);
         
            super.visitMultiANewArrayInsn(desc, dims);
        }

        @Override
        public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
            type = renameInternalType(type);
            
            super.visitTryCatchBlock(start, end, handler, type);
        }

        @Override
        public void visitLocalVariable(String name, String desc, String signature,
                Label start, Label end, int index) {
            desc = renameTypeDesc(desc);
            signature = renameFieldSignature(signature);
            
            super.visitLocalVariable(name, desc, signature, start, end, index);
        }

    }

    //----------------------------------
    
    public class RenameSignatureAdapter implements SignatureVisitor {

        private final SignatureVisitor mSv;

        public RenameSignatureAdapter(SignatureVisitor sv) {
            mSv = sv;
        }

        public void visitClassType(String name) {
            name = renameInternalType(name);
            mSv.visitClassType(name);
        }

        public void visitInnerClassType(String name) {
            name = renameInternalType(name);
            mSv.visitInnerClassType(name);
        }

        public SignatureVisitor visitArrayType() {
            SignatureVisitor sv = mSv.visitArrayType();
            return new RenameSignatureAdapter(sv);
        }

        public void visitBaseType(char descriptor) {
            mSv.visitBaseType(descriptor);
        }

        public SignatureVisitor visitClassBound() {
            SignatureVisitor sv = mSv.visitClassBound();
            return new RenameSignatureAdapter(sv);
        }

        public void visitEnd() {
            mSv.visitEnd();
        }

        public SignatureVisitor visitExceptionType() {
            SignatureVisitor sv = mSv.visitExceptionType();
            return new RenameSignatureAdapter(sv);
        }

        public void visitFormalTypeParameter(String name) {
            mSv.visitFormalTypeParameter(name);
        }

        public SignatureVisitor visitInterface() {
            SignatureVisitor sv = mSv.visitInterface();
            return new RenameSignatureAdapter(sv);
        }

        public SignatureVisitor visitInterfaceBound() {
            SignatureVisitor sv = mSv.visitInterfaceBound();
            return new RenameSignatureAdapter(sv);
        }

        public SignatureVisitor visitParameterType() {
            SignatureVisitor sv = mSv.visitParameterType();
            return new RenameSignatureAdapter(sv);
        }

        public SignatureVisitor visitReturnType() {
            SignatureVisitor sv = mSv.visitReturnType();
            return new RenameSignatureAdapter(sv);
        }

        public SignatureVisitor visitSuperclass() {
            SignatureVisitor sv = mSv.visitSuperclass();
            return new RenameSignatureAdapter(sv);
        }

        public void visitTypeArgument() {
            mSv.visitTypeArgument();
        }

        public SignatureVisitor visitTypeArgument(char wildcard) {
            SignatureVisitor sv = mSv.visitTypeArgument(wildcard);
            return new RenameSignatureAdapter(sv);
        }

        public void visitTypeVariable(String name) {
            mSv.visitTypeVariable(name);
        }
        
    }
}
