package net.ranides.assira.observable.impl;

import net.ranides.assira.events.EventRouter;
import net.ranides.assira.observable.BeanEvent;
import net.ranides.assira.reflection.*;
import net.ranides.assira.reflection.asm.*;
import net.ranides.assira.reflection.util.MemberUtils;

import java.util.Arrays;
import java.util.Collections;

public class CGModel<T> {

    private static final IClass<EventRouter> I_ROUTER = IClass.typeinfo(EventRouter.class);
    private static final IMethod M_SIGNAL = I_ROUTER.method("signalEvent").get();
    private static final String F_ROUTER = "router";
    private static final IClass<BeanEvent> I_EVENT = IClass.typeinfo(BeanEvent.class);
    private static final IMethod M_EVENT_CHANGE = I_EVENT.method("change").get();

    private final IClass<T> model;
    private final IClassLoader loader;
    private final String name;
    private final AsmClassBuilder writer;

    public CGModel(IClassLoader loader, IClass<T> model) {
        this.model = model;
        this.loader = loader;
        this.name = model.name() + "$asm:observable";
        this.writer = new AsmClassBuilder(IAttributes.of(IAttribute.PUBLIC), name, model, Collections.emptyList());
    }

    public IClass<T> generate() {
        for(IClass<?> c : new AsmInnerScanner().scan(model).result()) {
            writer.inner(c);
        }

        writer.field(IAttributes.of(IAttribute.PRIVATE), I_ROUTER, F_ROUTER);

        implementConstructor();

        for(IMethod m : model.methods()) {
            if(m.isSetter()) {
                if(m.isFinal()) {
                    throw new IllegalArgumentException(model + " is not observable because getter is final: " + m.name());
                }
                implementProperty(m);
            }
        }

        return (IClass)loader.defineIClass(name, writer.bytecode());
    }

    private void implementConstructor() {
        /// Adapter(Router router) {
        ///     super();
        ///     this.router = router;
        /// }

        AsmMethodBuilder m = writer.constructor(IAttributes.of(IAttribute.PUBLIC), Arrays.asList(I_ROUTER), Collections.emptyList());

        m.load(IClass.OBJECT, 0);
        m.invokeInit(model, Collections.emptyList());
        m.load(IClass.OBJECT, 0);
        m.load(IClass.OBJECT, 1);
        m.putField(writer.type(), I_ROUTER, F_ROUTER);
        m.returns();
        m.end();
    }

    private void implementProperty(IMethod setter) {
        /// void setProperty(T value) {
        ///     router.signalEvent(ModelEvent.change(
        ///         this,
        ///         "property",
        ///         getProperty(),
        ///         value
        ///     ));
        ///     super.setProperty(age);
        /// }

        IMethod getter = MemberUtils.asGetter(setter);

        AsmMethodBuilder m = writer.method(IAttributes.of(IAttribute.PUBLIC), setter.name(), setter);

        m.load(IClass.OBJECT, 0);                           // ...
        m.getField(writer.type(), I_ROUTER, F_ROUTER);      // router := this.router,

        m.load(IClass.OBJECT, 0);                           // this,
        m.push(MemberUtils.asPropertyName(setter.name()));  // "propertyName",
        m.load(IClass.OBJECT, 0);                           // ...
        m.invokeVirtual(writer.type(), getter);             // this.getProperty(),
        m.load(IClass.OBJECT, 1);                           // newValue (method 1st argument),

        m.invokeStatic(M_EVENT_CHANGE);                     // event := ModelEvent.change(this, "propertyName", getProperty(), newValue)

        m.invokeVirtual(I_ROUTER, M_SIGNAL);                // router.signal(event)

        m.pop();                                            // clear stack
        m.load(IClass.OBJECT, 0);                           // this
        m.load(IClass.OBJECT, 1);                           // newValue (method 1st argument)
        m.invokeSpecial(model, setter);                     // this.super.setProperty(newValue)

        m.returns();
        m.end();
    }

}
