package org.python.expose.generate;

import java.util.List;

import org.python.expose.MethodType;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodAdapter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.python.util.Generic;

/**
 * Visits a method passing all calls through to its delegate. If an ExposedNew or ExposedMethod
 * annotation is visited, calls handleResult with the exposer constructed with that annotation. Only
 * one of the handleResult methods will be called, if any.
 */
public abstract class ExposedMethodFinder extends MethodAdapter implements PyTypes, Opcodes {

    private Exposer newExp;

    private ExposedMethodVisitor methVisitor, classMethVisitor;

    private Type onType;

    private String methodDesc, typeName, methodName;

    private String[] exceptions;

    private int access;

    public ExposedMethodFinder(String typeName,
                               Type onType,
                               int access,
                               String name,
                               String desc,
                               String[] exceptions,
                               MethodVisitor delegate) {
        super(delegate);
        this.typeName = typeName;
        this.onType = onType;
        this.access = access;
        this.methodName = name;
        this.methodDesc = desc;
        this.exceptions = exceptions;
    }

    /**
     * @param exposer -
     *            the InstanceMethodExposer built as a result of visiting ExposeMethod
     */
    public abstract void handleResult(InstanceMethodExposer exposer);

    /**
     * @param exposer -
     *            the ClassMethodExposer built as a result of visiting ExposeClassMethod
     */
    public abstract void handleResult(ClassMethodExposer exposer);

    /**
     * @param exposer -
     *            the newExposer built as a result of visiting ExposeNew
     */
    public abstract void handleNewExposer(Exposer exposer);

    public abstract void exposeAsGetDescriptor(String descName, String doc);

    public abstract void exposeAsSetDescriptor(String descName);

    public abstract void exposeAsDeleteDescriptor(String descName);

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        if(desc.equals(EXPOSED_NEW.getDescriptor())) {
            if((access & ACC_STATIC) != 0) {
                newExp = new NewExposer(onType, access, methodName, methodDesc, exceptions);
            } else {
                newExp = new OverridableNewExposer(onType,
                                                   Type.getType("L" + onType.getInternalName()
                                                           + "Derived;"),
                                                   access,
                                                   methodName,
                                                   methodDesc,
                                                   exceptions);
            }
        } else if(desc.equals(EXPOSED_METHOD.getDescriptor())) {
            methVisitor = new ExposedMethodVisitor();
            return methVisitor;
        } else if(desc.equals(EXPOSED_CLASS_METHOD.getDescriptor())){
            classMethVisitor = new ExposedMethodVisitor();
            return classMethVisitor;
        } else if(desc.equals(EXPOSED_GET.getDescriptor())) {
            return new DescriptorVisitor(methodName) {

                @Override
                public void handleResult(String name, String doc) {
                    exposeAsGetDescriptor(name, doc);
                }
            };
        } else if(desc.equals(EXPOSED_SET.getDescriptor())) {
            return new DescriptorVisitor(methodName) {

                @Override
                public void handleResult(String name, String doc) {
                    exposeAsSetDescriptor(name);
                }
            };
        } else if(desc.equals(EXPOSED_DELETE.getDescriptor())) {
            return new DescriptorVisitor(methodName) {

                @Override
                public void handleResult(String name, String doc) {
                    exposeAsDeleteDescriptor(name);
                }
            };
        }
        return super.visitAnnotation(desc, visible);
    }

    private abstract class StringArrayBuilder extends RestrictiveAnnotationVisitor {

        @Override
        public void visit(String name, Object value) {
            vals.add((String)value);
        }

        @Override
        public void visitEnd() {
            handleResult(vals.toArray(new String[vals.size()]));
        }

        public abstract void handleResult(String[] result);

        List<String> vals = Generic.list();
    }

    class ExposedMethodVisitor extends RestrictiveAnnotationVisitor {

        @Override
        public void visit(String name, Object value) {
            if (name.equals("doc")) {
                doc = (String)value;
            } else {
                super.visit(name, value);
            }
        }

        @Override
        public AnnotationVisitor visitArray(String name) {
            if(name.equals("names")) {
                return new StringArrayBuilder() {

                    @Override
                    public void handleResult(String[] result) {
                        names = result;
                    }
                };
            } else if(name.equals("defaults")) {
                return new StringArrayBuilder() {

                    @Override
                    public void handleResult(String[] result) {
                        defaults = result;
                    }
                };
            } else {
                return super.visitArray(name);
            }
        }

        @Override
        public void visitEnum(String name, String desc, String value) {
            if(name.equals("type")) {
                type = MethodType.valueOf(value);
            } else {
                super.visitEnum(name, desc, value);
            }
        }

        private String[] names = new String[0];

        private String[] defaults = new String[0];

        private MethodType type = MethodType.DEFAULT;

        private String doc = "";
    }

    @Override
    public void visitEnd() {
        if(methVisitor != null) {
            handleResult(new InstanceMethodExposer(onType,
                                           access,
                                           methodName,
                                           methodDesc,
                                           typeName,
                                           methVisitor.names,
                                           methVisitor.defaults,
                                           methVisitor.type,
                                           methVisitor.doc));
        }
        if(newExp != null) {
            handleNewExposer(newExp);
        }
        if (classMethVisitor != null) {
            handleResult(new ClassMethodExposer(onType,
                                                access,
                                                methodName,
                                                methodDesc,
                                                typeName,
                                                classMethVisitor.names,
                                                classMethVisitor.defaults,
                                                classMethVisitor.doc));
        }
        super.visitEnd();
    }
}
