package net.ranides.assira.reflection.asm;

import net.ranides.assira.reflection.IAttribute;
import net.ranides.assira.reflection.IAttributes;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.reflection.IMethod;
import net.ranides.asm.ClassWriter;
import net.ranides.asm.Opcodes;

import java.util.Collections;

import static net.ranides.asm.ClassWriter.COMPUTE_FRAMES;
import static net.ranides.asm.Opcodes.ACC_SUPER;

/**
 * Facade for ASM ClassWriter.
 * It is compatible with reflective IClass interfaces.
 */
public class AsmClassBuilder {

    private final IClass<?> type;

    private final ClassWriter cv;

    /**
     * Creates class with specified attributes, name, parent and interfaces
     *
     * @param attrs attrs
     * @param name name
     * @param parent parent
     * @param interfaces interfaces
     */
    public AsmClassBuilder(IAttributes attrs, String name, IClass<?> parent, Iterable<IClass<?>> interfaces) {
        cv = new ClassWriter(COMPUTE_FRAMES);
        type = IClass.symbolic(name);
        cv.visit(Opcodes.V1_8, attrs.reflective() | ACC_SUPER, AsmSupport.path(type), null, AsmSupport.path(parent), AsmSupport.path(interfaces));
    }

    /**
     * Returns internal ClassWriter
     *
     * @return ClassWriter
     */
    public ClassWriter asm() {
        return cv;
    }

    /**
     * Returns type of generated class
     *
     * @return class
     */
    public IClass<?> type() {
        return type;
    }

    /**
     * Returns name of generated class
     *
     * @return string
     */
    public String name() {
        return type.name();
    }

    /**
     * Creates inner class with specified name & attributes.
     *
     * @param ref ref
     * @return this
     */
    public AsmClassBuilder inner(IClass<?> ref) {
        cv.visitInnerClass(AsmSupport.path(ref), AsmSupport.path(ref.outer()), ref.shortName(), ref.attributes().reflective());
        return this;
    }

    /**
     * Creates field with specified name, type and attributes
     *
     * @param attrs attrs
     * @param type type
     * @param name name
     * @return this
     */
    public AsmClassBuilder field(IAttributes attrs, IClass<?> type, String name) {
        cv.visitField(attrs.reflective(), name, AsmSupport.vm(type), AsmSupport.sig(type), null).visitEnd();
        return this;
    }

    /**
     * Creates constructor with specified attributes and signature
     *
     * @param attrs attrs
     * @param params parameters
     * @param exceptions exception list
     * @return AsmMethodBuilder
     */
    public AsmMethodBuilder constructor(IAttributes attrs, Iterable<IClass<?>> params, Iterable<IClass<?>> exceptions) {
        return method(attrs, "<init>", params, IClass.VOID, exceptions);
    }

    /**
     * Creates static initialization block
     *
     * @return AsmMethodBuilder
     */
    public AsmMethodBuilder init() {
        return method(IAttributes.of(IAttribute.STATIC), "<clinit>", Collections.emptyList(), IClass.VOID, Collections.emptyList());
    }

    /**
     * Creates method with specified attributes, name, and signature
     *
     * @param attrs attrs
     * @param name name
     * @param method signature
     * @return AsmMethodBuilder
     */
    public AsmMethodBuilder method(IAttributes attrs, String name, IMethod method) {
        return method(attrs, name, method.arguments().types(), method.returns(), method.exceptions());
    }

    /**
     * Creates method with specified attributes, name, and signature
     *
     * @param attrs attrs
     * @param name name
     * @param params params
     * @param ret return type
     * @param exceptions exception list
     * @return AsmMethodBuilder
     */
    public AsmMethodBuilder method(IAttributes attrs, String name, Iterable<IClass<?>> params, IClass<?> ret, Iterable<IClass<?>> exceptions) {
        return new AsmMethodBuilder(this, attrs, name, params, ret, exceptions);
    }

    /**
     * Generates class and returns generated bytecode.
     * Builder is unusable after invocation of this method.
     * Returned bytecode must be passed to some classloader or dumped into file for later use.
     *
     * @return bytecode
     */
    public byte[] bytecode() {
        cv.visitEnd();
        return cv.toByteArray();
    }

}
