/*
 *  Copyright (c) 2007 Mockito contributors
 *  This program is made available under the terms of the MIT License.
 */
package org.powermock.api.mockito.repackaged.cglib.transform.impl;

import org.powermock.api.mockito.repackaged.asm.Label;
import org.powermock.api.mockito.repackaged.asm.Type;
import org.powermock.api.mockito.repackaged.cglib.core.CodeEmitter;
import org.powermock.api.mockito.repackaged.cglib.core.Constants;
import org.powermock.api.mockito.repackaged.cglib.core.Local;
import org.powermock.api.mockito.repackaged.cglib.core.Signature;
import org.powermock.api.mockito.repackaged.cglib.core.TypeUtils;
import org.powermock.api.mockito.repackaged.cglib.transform.ClassEmitterTransformer;

/**
 * @author Juozas Baliuka, Chris Nokleberg
 */
public class InterceptFieldTransformer extends ClassEmitterTransformer {
    private static final String CALLBACK_FIELD = "$CGLIB_READ_WRITE_CALLBACK";
    private static final Type CALLBACK =
      TypeUtils.parseType("org.powermock.api.mockito.repackaged.cglib.transform.impl.InterceptFieldCallback");
    private static final Type ENABLED =
      TypeUtils.parseType("org.powermock.api.mockito.repackaged.cglib.transform.impl.InterceptFieldEnabled");
    private static final Signature ENABLED_SET =
      new Signature("setInterceptFieldCallback", Type.VOID_TYPE, new Type[]{ CALLBACK });
    private static final Signature ENABLED_GET =
      new Signature("getInterceptFieldCallback", CALLBACK, new Type[0]);

    private InterceptFieldFilter filter;
    
    public InterceptFieldTransformer(InterceptFieldFilter filter) {
        this.filter = filter;
    }
    
    private static Signature readMethodSig(String name, String desc) {
        return new Signature("$cglib_read_" + name, "()" + desc);
    }

    private static Signature writeMethodSig(String name, String desc) {
        return new Signature("$cglib_write_" + name, "(" + desc + ")V");
    }

    private static Signature readCallbackSig(Type type) {
        Type remap = remap(type);
        return new Signature("read" + callbackName(remap),
                             remap,
                             new Type[]{ Constants.TYPE_OBJECT,
                                         Constants.TYPE_STRING,
                                         remap });
    }

    private static Signature writeCallbackSig(Type type) {
        Type remap = remap(type);
        return new Signature("write" + callbackName(remap),
                             remap,
                             new Type[]{ Constants.TYPE_OBJECT,
                                         Constants.TYPE_STRING,
                                         remap,
                                         remap });
    }
                
    private static Type remap(Type type) {
        switch (type.getSort()) {
        case Type.OBJECT:
        case Type.ARRAY:
            return Constants.TYPE_OBJECT;
        default:
            return type;
        }
    }

    private static String callbackName(Type type) {
        return (type == Constants.TYPE_OBJECT) ?
            "Object" :
            TypeUtils.upperFirst(TypeUtils.getClassName(type));
    }

    public void begin_class(int version, int access, String className, Type superType, Type[] interfaces, String sourceFile) {
        if (!TypeUtils.isInterface(access)) {
            super.begin_class(version, access, className, superType, TypeUtils.add(interfaces, ENABLED), sourceFile);

            super.declare_field(Constants.ACC_PRIVATE | Constants.ACC_TRANSIENT,
                                CALLBACK_FIELD,
                                CALLBACK,
                                null);

            CodeEmitter e;
            e = super.begin_method(Constants.ACC_PUBLIC, ENABLED_GET, null);
            e.load_this();
            e.getfield(CALLBACK_FIELD);
            e.return_value();
            e.end_method();

            e = super.begin_method(Constants.ACC_PUBLIC, ENABLED_SET, null);
            e.load_this();
            e.load_arg(0);
            e.putfield(CALLBACK_FIELD);
            e.return_value();
            e.end_method();
        } else {
            super.begin_class(version, access, className, superType, interfaces, sourceFile);
        }
    }

    public void declare_field(int access, String name, Type type, Object value) {
        super.declare_field(access, name, type, value);
        if (!TypeUtils.isStatic(access)) {
            if (filter.acceptRead(getClassType(), name)) {
                addReadMethod(name, type);
            }
            if (filter.acceptWrite(getClassType(), name)) {
                addWriteMethod(name, type);
            }
        }
    }

    private void addReadMethod(String name, Type type) {
        CodeEmitter e = super.begin_method(Constants.ACC_PUBLIC,
                                           readMethodSig(name, type.getDescriptor()),
                                           null);
        e.load_this();
        e.getfield(name);
        e.load_this();
        e.invoke_interface(ENABLED,ENABLED_GET);
        Label intercept = e.make_label();
        e.ifnonnull(intercept);
        e.return_value();

        e.mark(intercept);
        Local result = e.make_local(type);
        e.store_local(result);
        e.load_this();
        e.invoke_interface(ENABLED,ENABLED_GET);
        e.load_this();
        e.push(name);
        e.load_local(result);
        e.invoke_interface(CALLBACK, readCallbackSig(type));
        if (!TypeUtils.isPrimitive(type)) {
            e.checkcast(type);
        }
        e.return_value();
        e.end_method();
    }

    private void addWriteMethod(String name, Type type) {
        CodeEmitter e = super.begin_method(Constants.ACC_PUBLIC,
                                           writeMethodSig(name, type.getDescriptor()),
                                           null);
        e.load_this();
        e.dup();
        e.invoke_interface(ENABLED,ENABLED_GET);
        Label skip = e.make_label();
        e.ifnull(skip);

        e.load_this();
        e.invoke_interface(ENABLED,ENABLED_GET);
        e.load_this();
        e.push(name);
        e.load_this();
        e.getfield(name);
        e.load_arg(0);
        e.invoke_interface(CALLBACK, writeCallbackSig(type));
        if (!TypeUtils.isPrimitive(type)) {
            e.checkcast(type);
        }
        Label go = e.make_label();
        e.goTo(go);
        e.mark(skip);
        e.load_arg(0);
        e.mark(go);
        e.putfield(name);
        e.return_value();
        e.end_method();
    }

    public CodeEmitter begin_method(int access, Signature sig, Type[] exceptions) {
        return new CodeEmitter(super.begin_method(access, sig, exceptions)) {
            public void visitFieldInsn(int opcode, String owner, String name, String desc) {
                Type towner = TypeUtils.fromInternalName(owner);
                switch (opcode) {
                case Constants.GETFIELD:
                    if (filter.acceptRead(towner, name)) {
                        helper(towner, readMethodSig(name, desc));
                        return;
                    }
                    break;
                case Constants.PUTFIELD:
                    if (filter.acceptWrite(towner, name)) {
                        helper(towner, writeMethodSig(name, desc));
                        return;
                    }
                    break;
                }
                super.visitFieldInsn(opcode, owner, name, desc);
            }

            private void helper(Type owner, Signature sig) {
                invoke_virtual(owner, sig);
            }
        };
    }
}
