/*
 * 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.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import java.util.Set;

/**
 * Class adapter that can stub some or all of the methods of the class.
 */
class TransformClassAdapter extends ClassVisitor {

    /** True if all methods should be stubbed, false if only native ones must be stubbed. */
    private final boolean mStubAll;
    /** True if the class is an interface. */
    private boolean mIsInterface;
    private final String mClassName;
    private final Log mLog;
    private final Set<String> mStubMethods;
    private Set<String> mDeleteReturns;

    /**
     * Creates a new class adapter that will stub some or all methods.
     * @param logger
     * @param stubMethods  list of method signatures to always stub out
     * @param deleteReturns list of types that trigger the deletion of methods returning them.
     * @param className The name of the class being modified
     * @param cv The parent class writer visitor
     * @param stubNativesOnly True if only native methods should be stubbed. False if all
     *                        methods should be stubbed.
     * @param hasNative True if the method has natives, in which case its access should be
     *                  changed.
     */
    public TransformClassAdapter(Log logger, Set<String> stubMethods,
            Set<String> deleteReturns, String className, ClassVisitor cv,
            boolean stubNativesOnly, boolean hasNative) {
        super(Opcodes.ASM4, cv);
        mLog = logger;
        mStubMethods = stubMethods;
        mClassName = className;
        mStubAll = !stubNativesOnly;
        mIsInterface = false;
        mDeleteReturns = deleteReturns;
    }

    /* Visits the class header. */
    @Override
    public void visit(int version, int access, String name,
            String signature, String superName, String[] interfaces) {

        // This class might be being renamed.
        name = mClassName;

        // remove protected or private and set as public
        if (Main.sOptions.generatePublicAccess) {
            access = access & ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED);
            access |= Opcodes.ACC_PUBLIC;
        }
        // remove final
        access = access & ~Opcodes.ACC_FINAL;
        // note: leave abstract classes as such
        // don't try to implement stub for interfaces

        mIsInterface = ((access & Opcodes.ACC_INTERFACE) != 0);
        super.visit(version, access, name, signature, superName, interfaces);
    }

    /* Visits the header of an inner class. */
    @Override
    public void visitInnerClass(String name, String outerName, String innerName, int access) {
        // remove protected or private and set as public
        if (Main.sOptions.generatePublicAccess) {
            access = access & ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED);
            access |= Opcodes.ACC_PUBLIC;
        }
        // remove final
        access = access & ~Opcodes.ACC_FINAL;
        // note: leave abstract classes as such
        // don't try to implement stub for interfaces

        super.visitInnerClass(name, outerName, innerName, access);
    }

    /* Visits a method. */
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
            String signature, String[] exceptions) {

        if (mDeleteReturns != null) {
            Type t = Type.getReturnType(desc);
            if (t.getSort() == Type.OBJECT) {
                String returnType = t.getInternalName();
                if (returnType != null) {
                    if (mDeleteReturns.contains(returnType)) {
                        return null;
                    }
                }
            }
        }

        String methodSignature = mClassName.replace('/', '.') + "#" + name;

        // change access to public
        if (Main.sOptions.generatePublicAccess) {
            access &= ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE);
            access |= Opcodes.ACC_PUBLIC;
        }

        // remove final
        access = access & ~Opcodes.ACC_FINAL;

        // stub this method if they are all to be stubbed or if it is a native method
        // and don't try to stub interfaces nor abstract non-native methods.
        if (!mIsInterface &&
            ((access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) != Opcodes.ACC_ABSTRACT) &&
            (mStubAll ||
             (access & Opcodes.ACC_NATIVE) != 0) ||
             mStubMethods.contains(methodSignature)) {

            boolean isStatic = (access & Opcodes.ACC_STATIC) != 0;
            boolean isNative = (access & Opcodes.ACC_NATIVE) != 0;

            // remove abstract, final and native
            access = access & ~(Opcodes.ACC_ABSTRACT | Opcodes.ACC_FINAL | Opcodes.ACC_NATIVE);

            String invokeSignature = methodSignature + desc;
            mLog.debug("  Stub: %s (%s)", invokeSignature, isNative ? "native" : "");

            MethodVisitor mw = super.visitMethod(access, name, desc, signature, exceptions);
            return new StubMethodAdapter(mw, name, returnType(desc), invokeSignature,
                    isStatic, isNative);

        } else {
            mLog.debug("  Keep: %s %s", name, desc);
            return super.visitMethod(access, name, desc, signature, exceptions);
        }
    }

    /* Visits a field. Makes it public. */
    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature,
            Object value) {
        // change access to public
        if (Main.sOptions.generatePublicAccess) {
            access &= ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE);
            access |= Opcodes.ACC_PUBLIC;
        }
        return super.visitField(access, name, desc, signature, value);
    }

    /**
     * Extracts the return {@link Type} of this descriptor.
     */
    Type returnType(String desc) {
        if (desc != null) {
            try {
                return Type.getReturnType(desc);
            } catch (ArrayIndexOutOfBoundsException e) {
                // ignore, not a valid type.
            }
        }
        return null;
    }
}
