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

import org.powermock.api.mockito.repackaged.asm.ClassAdapter;
import org.powermock.api.mockito.repackaged.asm.ClassVisitor;
import org.powermock.api.mockito.repackaged.asm.FieldVisitor;
import org.powermock.api.mockito.repackaged.asm.MethodAdapter;
import org.powermock.api.mockito.repackaged.asm.MethodVisitor;
import org.powermock.api.mockito.repackaged.asm.Type;

import java.util.HashMap;
import java.util.Map;

/**
 * @author Juozas Baliuka, Chris Nokleberg
 */
public class ClassEmitter extends ClassAdapter {
    private static int hookCounter;
    private ClassInfo classInfo;
    private Map fieldInfo;
    private MethodVisitor rawStaticInit;
    private CodeEmitter staticInit;
    private CodeEmitter staticHook;
    private Signature staticHookSig;

    public ClassEmitter(ClassVisitor cv) {
        super(null);
        setTarget(cv);
    }

    public ClassEmitter() {
        super(null);
    }

    synchronized private static int getNextHook() {
        return ++hookCounter;
    }

    public void setTarget(ClassVisitor cv) {
        this.cv = cv;
        fieldInfo = new HashMap();

        // just to be safe
        staticInit = staticHook = null;
        staticHookSig = null;
    }

    public ClassInfo getClassInfo() {
        return classInfo;
    }

    public void begin_class(int version, final int access, String className, final Type superType, final Type[] interfaces, String source) {
        final Type classType = Type.getType("L" + className.replace('.', '/') + ";");
        classInfo = new ClassInfo() {
            public Type getType() {
                return classType;
            }
            public Type getSuperType() {
                return (superType != null) ? superType : Constants.TYPE_OBJECT;
            }
            public Type[] getInterfaces() {
                return interfaces;
            }
            public int getModifiers() {
                return access;
            }
        };
        cv.visit(version,
                 access,
                 classInfo.getType().getInternalName(),
                 null,
                 classInfo.getSuperType().getInternalName(),
                 TypeUtils.toInternalNames(interfaces));
        if (source != null)
            cv.visitSource(source, null);
        init();
    }

    public CodeEmitter getStaticHook() {
         if (TypeUtils.isInterface(getAccess())) {
             throw new IllegalStateException("static hook is invalid for this class");
         }
         if (staticHook == null) {
             staticHookSig = new Signature("CGLIB$STATICHOOK" + getNextHook(), "()V");
             staticHook = begin_method(Constants.ACC_STATIC,
                                       staticHookSig,
                                       null);
             if (staticInit != null) {
                 staticInit.invoke_static_this(staticHookSig);
             }
         }
         return staticHook;
    }

    protected void init() {
    }

    public int getAccess() {
        return classInfo.getModifiers();
    }

    public Type getClassType() {
        return classInfo.getType();
    }

    public Type getSuperType() {
        return classInfo.getSuperType();
    }

    public void end_class() {
        if (staticHook != null && staticInit == null) {
            // force creation of static init
            begin_static();
        }
        if (staticInit != null) {
            staticHook.return_value();
            staticHook.end_method();
            rawStaticInit.visitInsn(Constants.RETURN);
            rawStaticInit.visitMaxs(0, 0);
            staticInit = staticHook = null;
            staticHookSig = null;
        }
        cv.visitEnd();
    }

    public CodeEmitter begin_method(int access, Signature sig, Type[] exceptions) {
        if (classInfo == null)
            throw new IllegalStateException("classInfo is null! " + this);
        MethodVisitor v = cv.visitMethod(access,
                                         sig.getName(),
                                         sig.getDescriptor(),
                                         null,
                                         TypeUtils.toInternalNames(exceptions));
        if (sig.equals(Constants.SIG_STATIC) && !TypeUtils.isInterface(getAccess())) {
            rawStaticInit = v;
            MethodVisitor wrapped = new MethodAdapter(v) {
                public void visitMaxs(int maxStack, int maxLocals) {
                    // ignore
                }
                public void visitInsn(int insn) {
                    if (insn != Constants.RETURN) {
                        super.visitInsn(insn);
                    }
                }
            };
            staticInit = new CodeEmitter(this, wrapped, access, sig, exceptions);
            if (staticHook == null) {
                // force static hook creation
                getStaticHook();
            } else {
                staticInit.invoke_static_this(staticHookSig);
            }
            return staticInit;
        } else if (sig.equals(staticHookSig)) {
            return new CodeEmitter(this, v, access, sig, exceptions) {
                public boolean isStaticHook() {
                    return true;
                }
            };
        } else {
            return new CodeEmitter(this, v, access, sig, exceptions);
        }
    }

    public CodeEmitter begin_static() {
        return begin_method(Constants.ACC_STATIC, Constants.SIG_STATIC, null);
    }

    public void declare_field(int access, String name, Type type, Object value) {
        FieldInfo existing = (FieldInfo)fieldInfo.get(name);
        FieldInfo info = new FieldInfo(access, name, type, value);
        if (existing != null) {
            if (!info.equals(existing)) {
                throw new IllegalArgumentException("Field \"" + name + "\" has been declared differently");
            }
        } else {
            fieldInfo.put(name, info);
            cv.visitField(access, name, type.getDescriptor(), null, value);
        }
    }

    // TODO: make public?
    boolean isFieldDeclared(String name) {
        return fieldInfo.get(name) != null;
    }

    FieldInfo getFieldInfo(String name) {
        FieldInfo field = (FieldInfo)fieldInfo.get(name);
        if (field == null) {
            throw new IllegalArgumentException("Field " + name + " is not declared in " + getClassType().getClassName());
        }
        return field;
    }
    
    public void visit(int version,
                      int access,
                      String name,
                      String signature,
                      String superName,
                      String[] interfaces) {
        begin_class(version,
                    access,
                    name.replace('/', '.'),
                    TypeUtils.fromInternalName(superName),
                    TypeUtils.fromInternalNames(interfaces),
                    null); // TODO
    }

    public void visitEnd() {
        end_class();
    }
    
    public FieldVisitor visitField(int access,
                                   String name,
                                   String desc,
                                   String signature,
                                   Object value) {
        declare_field(access, name, Type.getType(desc), value);
        return null; // TODO
    }
    
    public MethodVisitor visitMethod(int access,
                                     String name,
                                     String desc,
                                     String signature,
                                     String[] exceptions) {
        return begin_method(access,
                            new Signature(name, desc),
                            TypeUtils.fromInternalNames(exceptions));
    }
    
    static class FieldInfo {
        int access;
        String name;
        Type type;
        Object value;

        public FieldInfo(int access, String name, Type type, Object value) {
            this.access = access;
            this.name = name;
            this.type = type;
            this.value = value;
        }

        public boolean equals(Object o) {
            if (o == null)
                return false;
            if (!(o instanceof FieldInfo))
                return false;
            FieldInfo other = (FieldInfo)o;
            if (access != other.access ||
                !name.equals(other.name) ||
                !type.equals(other.type)) {
                return false;
            }
            if ((value == null) ^ (other.value == null))
                return false;
            if (value != null && !value.equals(other.value))
                return false;
            return true;
        }

        public int hashCode() {
            return access ^ name.hashCode() ^ type.hashCode() ^ ((value == null) ? 0 : value.hashCode());
        }
    }
}
