package io.quarkus.funqy.deployment;

import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;

import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiFunction;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.BuiltinScope;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.arc.processor.Transformation;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.recording.RecorderContext;
import io.quarkus.funqy.Funq;
import io.quarkus.funqy.runtime.FunctionRecorder;
import io.quarkus.gizmo.Gizmo;

public class FunctionScannerBuildStep {
    public static final DotName FUNQ = DotName.createSimple(Funq.class.getName());

    @BuildStep
    public void scanFunctions(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem,
            BuildProducer<BytecodeTransformerBuildItem> transformers,
            BuildProducer<AnnotationsTransformerBuildItem> annotationsTransformer,
            BuildProducer<UnremovableBeanBuildItem> unremovableBeans,
            BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
            BuildProducer<FunctionBuildItem> functions) {
        Collection<AnnotationInstance> funqs = beanArchiveIndexBuildItem.getIndex().getAnnotations(FUNQ);
        Set<ClassInfo> classes = new HashSet<>();
        Set<String> classNames = new HashSet<>();
        for (AnnotationInstance funqMethod : funqs) {
            MethodInfo method = funqMethod.target().asMethod();
            String className = method.declaringClass().name().toString();
            classNames.add(className);
            classes.add(method.declaringClass());
            String methodName = method.name();
            String functionName = null;
            if (funqMethod.value() != null) {
                functionName = funqMethod.value().asString();
            }
            if (functionName != null && functionName.isEmpty())
                functionName = null;
            functions.produce(new FunctionBuildItem(className, methodName, functionName));
        }
        Set<ClassInfo> withoutDefaultCtor = new HashSet<>();
        for (ClassInfo clazz : classes) {
            reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, true, clazz.name().toString()));
            if (!clazz.hasNoArgsConstructor()) {
                withoutDefaultCtor.add(clazz);
            }
        }
        unremovableBeans.produce(new UnremovableBeanBuildItem(b -> classNames.contains(b.getBeanClass().toString())));
        generateDefaultConstructors(transformers, withoutDefaultCtor);
        annotationsTransformer.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() {

            @Override
            public boolean appliesTo(AnnotationTarget.Kind kind) {
                return kind == AnnotationTarget.Kind.CLASS;
            }

            @Override
            public void transform(TransformationContext transformationContext) {
                ClassInfo clazz = transformationContext.getTarget().asClass();
                if (!classes.contains(clazz))
                    return;
                Transformation transformation = transformationContext.transform()
                        .add(BuiltinScope.DEPENDENT.getName());
                if (clazz.classAnnotation(DotNames.TYPED) == null) {
                    // Add @Typed(MySubresource.class)
                    transformation.add(createTypedAnnotationInstance(clazz));
                }
                transformation.done();
            }
        }));
    }

    @BuildStep
    @Record(STATIC_INIT)
    public FunctionInitializedBuildItem staticInit(FunctionRecorder recorder, List<FunctionBuildItem> functions,
            RecorderContext context) {
        if (functions == null || functions.isEmpty())
            return null;
        recorder.init();
        for (FunctionBuildItem function : functions) {
            if (function.getFunctionName() == null) {
                recorder.register(context.classProxy(function.getClassName()), function.getMethodName());
            } else {
                recorder.register(context.classProxy(function.getClassName()), function.getMethodName(),
                        function.getFunctionName());
            }
        }
        return FunctionInitializedBuildItem.SINGLETON;
    }

    private static void generateDefaultConstructors(BuildProducer<BytecodeTransformerBuildItem> transformers,
            Set<ClassInfo> withoutDefaultCtor) {

        for (ClassInfo classInfo : withoutDefaultCtor) {
            // keep it super simple - only generate default constructor is the object is a direct descendant of Object
            if (!(classInfo.superClassType() != null && classInfo.superClassType().name().equals(DotNames.OBJECT))) {
                return;
            }

            final String name = classInfo.name().toString();
            transformers
                    .produce(new BytecodeTransformerBuildItem(name, new BiFunction<String, ClassVisitor, ClassVisitor>() {
                        @Override
                        public ClassVisitor apply(String className, ClassVisitor classVisitor) {
                            ClassVisitor cv = new ClassVisitor(Gizmo.ASM_API_VERSION, classVisitor) {

                                @Override
                                public void visit(int version, int access, String name, String signature, String superName,
                                        String[] interfaces) {
                                    super.visit(version, access, name, signature, superName, interfaces);
                                    MethodVisitor ctor = visitMethod(Modifier.PUBLIC, "<init>", "()V", null,
                                            null);
                                    ctor.visitCode();
                                    ctor.visitVarInsn(Opcodes.ALOAD, 0);
                                    ctor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
                                    ctor.visitInsn(Opcodes.RETURN);
                                    ctor.visitMaxs(1, 1);
                                    ctor.visitEnd();
                                }
                            };
                            return cv;
                        }
                    }));
        }
    }

    private AnnotationInstance createTypedAnnotationInstance(ClassInfo clazz) {
        return AnnotationInstance.create(DotNames.TYPED, clazz,
                new AnnotationValue[] { AnnotationValue.createArrayValue("value",
                        new AnnotationValue[] { AnnotationValue.createClassValue("value",
                                Type.create(clazz.name(), org.jboss.jandex.Type.Kind.CLASS)) }) });
    }

}
