package org.python.expose.generate;

import org.objectweb.asm.Type;
import org.python.core.PyDataDescr;

/**
 * Generates a class to expose a descriptor on Python type. One of addMethodGetter or addFieldGetter
 * must be called, and possibly one of addMethodSetter and addFieldSetter if this is a settable
 * descriptor. If this is a deletable descriptor, addMethodDeleter may be called. There is no
 * addFieldDeleter since there's no defined behavior to 'delete' a field.
 */
public class DescriptorExposer extends Exposer {

    private Type onType, ofType;

    private String name;

    private String doc;

    private String getterMethodName, getterFieldName, setterMethodName, setterFieldName,
            deleterMethodName;

    /**
     * Creates an exposer that will work on type and have <code>descrName</code> as its name in
     * the type's dict.
     */
    public DescriptorExposer(Type onType, String descrName) {
        super(PyDataDescr.class, onType.getClassName() + "$" + descrName + "_descriptor", ASSUPER);
        this.onType = onType;
        name = descrName;
    }

    /**
     * @return - the name this descriptor will be exposed as in its type's dict
     */
    public String getName() {
        return name;
    }

    public void addMethodGetter(String methodName, String desc) {
        addMethodGetter(methodName, desc, null);
    }

    public void addMethodGetter(String methodName, String desc, String doc) {
        if(hasGetter()) {
            error("Descriptor can only have one getter");
        }
        if(Type.getArgumentTypes(desc).length > 0) {
            error("Getter can't take arguments");
        }
        setOfType(Type.getReturnType(desc));
        getterMethodName = methodName;
        this.doc = doc;
    }

    public void addFieldGetter(String fieldName, Type fieldType) {
        addFieldGetter(fieldName, fieldType, null);
    }

    public void addFieldGetter(String fieldName, Type fieldType, String doc) {
        if(hasGetter()) {
            error("Can only have one getter for a descriptor");
        }
        setOfType(fieldType);
        getterFieldName = fieldName;
        this.doc = doc;
    }

    public boolean hasGetter() {
        return getterMethodName != null || getterFieldName != null;
    }

    private void setOfType(Type type) {
        if(ofType == null) {
            ofType = type;
        } else if(!ofType.equals(type)) {
            error("Types of the getter and setter must agree");
        }
    }

    public void addMethodSetter(String methodName, String desc) {
        if(hasSetter()) {
            error("Descriptor can only have one setter");
        }
        Type[] args = Type.getArgumentTypes(desc);
        if(args.length > 1) {
            error("Setter can only take one argument");
        }
        setOfType(args[0]);
        setterMethodName = methodName;
    }

    public void addFieldSetter(String fieldName, Type fieldType) {
        if(hasSetter()) {
            error("Descriptor can only have one setter");
        }
        setOfType(fieldType);
        setterFieldName = fieldName;
    }

    public boolean hasSetter() {
        return setterMethodName != null || setterFieldName != null;
    }

    public void addMethodDeleter(String methodName, String desc) {
        deleterMethodName = methodName;
    }

    public String toString() {
        return "DescriptorExposer[class=" + onType.getClassName() + ", name=" + name + "]";
    }

    @Override
    protected void generate() {
        if(!hasGetter()) {
            error("A descriptor requires at least a get");
        }
        generateConstructor();
        if(getterMethodName != null) {
            generateMethodGetter();
        } else {
            generateFieldGetter();
        }
        generateImplement("Get", getterMethodName != null || getterFieldName != null);
        if(setterMethodName != null) {
            generateMethodSetter();
        } else if(setterFieldName != null) {
            generateFieldSetter();
        }
        generateImplement("Set", setterMethodName != null || setterFieldName != null);
        if(deleterMethodName != null) {
            generateMethodDeleter();
        }
        generateImplement("Delete", deleterMethodName != null);
    }

    private void generateConstructor() {
        startConstructor();
        mv.visitVarInsn(ALOAD, 0);
        mv.visitLdcInsn(name);
        if(PRIMITIVES.containsKey(ofType)) {
            mv.visitLdcInsn(PRIMITIVES.get(ofType));
        } else {
            mv.visitLdcInsn(ofType);
        }
        if (doc == null) {
            mv.visitInsn(ACONST_NULL);
        } else {
            mv.visitLdcInsn(doc);
        }
        superConstructor(STRING, CLASS, STRING);
        endConstructor();
    }

    private void generateMethodGetter() {
        startMethod("invokeGet", OBJECT, PYOBJ);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitTypeInsn(CHECKCAST, onType.getInternalName());
        call(onType, getterMethodName, ofType);
        if(PRIMITIVES.containsKey(ofType) || ofType.equals(STRING)) {
            toPy(ofType);
        }
        endMethod(ARETURN);
    }

    private void generateFieldGetter() {
        startMethod("invokeGet", OBJECT, PYOBJ);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitTypeInsn(CHECKCAST, onType.getInternalName());
        mv.visitFieldInsn(GETFIELD,
                          onType.getInternalName(),
                          getterFieldName,
                          ofType.getDescriptor());
        if(PRIMITIVES.containsKey(ofType) || ofType.equals(STRING)) {
            toPy(ofType);
        }
        endMethod(ARETURN);
    }

    private void generateMethodSetter() {
        startMethod("invokeSet", VOID, PYOBJ, OBJECT);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitTypeInsn(CHECKCAST, onType.getInternalName());
        mv.visitVarInsn(ALOAD, 2);
        if(PRIMITIVES.containsKey(ofType)) {
            mv.visitTypeInsn(CHECKCAST, PRIMITIVES.get(ofType).getInternalName());
            call(PRIMITIVES.get(ofType), ofType.getClassName() + "Value", ofType);
        } else {
            mv.visitTypeInsn(CHECKCAST, ofType.getInternalName());
        }
        call(onType, setterMethodName, VOID, ofType);
        endMethod(RETURN);
    }

    private void generateFieldSetter() {
        startMethod("invokeSet", VOID, PYOBJ, OBJECT);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitTypeInsn(CHECKCAST, onType.getInternalName());
        mv.visitVarInsn(ALOAD, 2);
        if(PRIMITIVES.containsKey(ofType)) {
            mv.visitTypeInsn(CHECKCAST, PRIMITIVES.get(ofType).getInternalName());
            call(PRIMITIVES.get(ofType), ofType.getClassName() + "Value", ofType);
        } else {
            mv.visitTypeInsn(CHECKCAST, ofType.getInternalName());
        }
        mv.visitFieldInsn(PUTFIELD,
                          onType.getInternalName(),
                          setterFieldName,
                          ofType.getDescriptor());
        endMethod(RETURN);
    }

    private void generateMethodDeleter() {
        startMethod("invokeDelete", VOID, PYOBJ);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitTypeInsn(CHECKCAST, onType.getInternalName());
        call(onType, deleterMethodName, VOID);
        endMethod(RETURN);
    }

    private void generateImplement(String setOrDelete, boolean implementsIt) {
        startMethod("implementsDescr" + setOrDelete, BOOLEAN);
        mv.visitInsn(implementsIt ? ICONST_1 : ICONST_0);
        endMethod(IRETURN);
    }

    private void error(String reason) {
        throw new InvalidExposingException(reason + "[class=" + onType.getClassName() + ", name="
                + name + "]");
    }
}
