package net.ranides.assira.reflection.impl;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import net.ranides.assira.annotations.Meta;
import net.ranides.assira.generic.SerializationUtils;
import net.ranides.assira.reflection.*;
import net.ranides.assira.reflection.asm.*;

import java.io.Serializable;
import java.util.*;
import java.util.function.Function;

/**
 * Internal class which generates bytecdode for adapter.
 *
 * Adapter implements class A, but delegates all method class to object of type B.
 * Generally, it matches methods by name.
 *
 * Please note that this class stores a lot of state only because we share information
 * across private methods, during bytecode generation.
 *
 * Generated class could by cached by using (A,B) pair as a key.
 */
@Slf4j
public class CGAdapter implements Serializable {

    private static final String F_THIS = "this";

    private static final String F_THAT = "that";

    private static final String F_METHODS = "method_";

    private static final IAttributes A_METHODS = IAttributes.of(IAttribute.PUBLIC, IAttribute.SYNTHETIC, IAttribute.STATIC);

    private static final String F_ADAPTERS = "adapter_";

    private static final IAttributes A_ADAPTERS = IAttributes.of(IAttribute.PUBLIC, IAttribute.SYNTHETIC, IAttribute.STATIC);

    private static final String M_THAT_NAME = "net_ranides_assira_cg_that";

    private static final IMethod M_THAT = IClass.typeinfo(CGAdapterWrapper.class).method(M_THAT_NAME).get();

    private final AdapterFactory factory;

    private final IClass<?> adapter;

    private final IClass<?> that;

    private final String name;

    private final Set<IClass<?>> inner;

    private final AsmClassBuilder writer;

    private final List<IMethod> reflective;

    private final List<AdapterMeta> adapters;

    /**
     * Generates class of object "adapter" which will delegate work to object for type "that".
     * Generated class has one-argument constructor, which takes "that" as argument.
     *
     * Instances of adapter class can be serialized as long, as "that" object is serializable.
     * They will be recreated using named IClassLoader provided by AdapterFactory.
     *
     * This generator will use AdapterFactory to create recursive adapters for arguments and return types.
     *
     * @param factory factory
     * @param adapter adapter
     * @param that that
     * @return "adapter" implementation
     */
    public static IClass<?> generate(AdapterFactory factory, IClass<?> adapter, IClass<?> that) {
        return new CGAdapter(factory, adapter, that).generate();
    }

    private CGAdapter(AdapterFactory factory, IClass<?> adapter, IClass<?> that) {
        this.factory = factory;
        this.adapter = adapter;
        this.that = that;
        this.name = that.name() + "$asm:" + AsmSupport.escape(adapter.name());
        this.reflective = new ArrayList<>();
        this.adapters = new ArrayList<>();
        this.inner = new AsmInnerScanner().scan(adapter).scan(that).result();
        this.writer = new AsmClassBuilder(IAttributes.of(IAttribute.PUBLIC), name, IClass.OBJECT, Arrays.asList(adapter, IClass.typeinfo(CGAdapterWrapper.class)));
    }

    private IClass<?> generate() {
        for(IClass<?> c : inner) {
            writer.inner(c);
        }

        writer.field(IAttributes.of(IAttribute.PRIVATE), that, F_THAT);

        implementConstructor();

        implementInterface();

        implementToString();

        implementWriteReplace();

        for(IMethod m : adapter.methods()) {
            if (m.attributes().has(IAttribute.ABSTRACT)) {
                new MethodLink(m).link();
            }
        }

        declareStatic();

        IClass<?> generated = factory.loader().defineIClass(name, writer.bytecode());
        initStatic(generated);
        return generated;
    }

    private void declareStatic() {
        for(int i=0, n=reflective.size(); i<n; i++) {
            writer.field(A_METHODS, IClass.typeinfo(IMethod.class), F_METHODS+i);
        }
        for(int i=0, n=adapters.size(); i<n; i++) {
            writer.field(A_ADAPTERS, IClass.typeinfo(Function.class), F_ADAPTERS + i);
        }
    }

    private void initStatic(IClass<?> generated) {
        for(int i=0, n=reflective.size(); i<n; i++) {
            generated.field(F_METHODS+i).get().set(null, reflective.get(i));
        }

        for(int i=0, n=adapters.size(); i<n; i++) {
            AdapterMeta meta = adapters.get(i);
            generated.field(F_ADAPTERS+i).get().set(null, this.factory.adapter(meta.target, meta.source));
        }
    }

    private void implementConstructor() {
        /// Adapter(That that) {
        ///     super();
        ///     this.that = that;
        /// }

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

        m.load(IClass.OBJECT, 0);
        m.invokeInit(IClass.OBJECT, Collections.emptyList());
        m.load(IClass.OBJECT, 0);
        m.load(IClass.OBJECT, 1);
        m.putField(writer.type(), that, F_THAT);
        m.returns();

        m.var(writer.type(), F_THIS);
        m.var(that, F_THAT);
        m.end();
    }

    private void implementToString() {
        /// String toString() {
        ///     String v = that.toString();
        ///     return new StringBuilder(24 + v.length()).append("Adapter{").append(that).append("=> some type}").toString()
        /// }

        String prefix = "Adapter{";
        String suffix = " => "+adapter.shortName()+"}";
        IClass<StringBuilder> sb = IClass.typeinfo(StringBuilder.class);

        AsmMethodBuilder m = writer.method(IAttributes.of(IAttribute.PUBLIC), "toString", Collections.emptyList(), IClass.STRING, Collections.emptyList());

        m.load(IClass.OBJECT, 0);
        m.getField(writer.type(), that, F_THAT);
        m.invokeVirtual(IClass.OBJECT, "toString", Collections.emptyList(), IClass.STRING);
        m.store(IClass.OBJECT, 1);

        m.newObject(sb);
        m.dup();

        m.push(prefix.length()+suffix.length());
        m.load(IClass.OBJECT, 1);
        m.invokeVirtual(IClass.STRING, "length", Collections.emptyList(), IClass.INT);
        m.add(IClass.INT);
        m.invokeInit(sb, Arrays.asList(IClass.INT));

        m.push(prefix);
        m.invokeVirtual(sb, "append", Arrays.asList(IClass.STRING), sb);
        m.load(IClass.OBJECT, 1);
        m.invokeVirtual(sb, "append", Arrays.asList(IClass.STRING), sb);
        m.push(suffix);
        m.invokeVirtual(sb, "append", Arrays.asList(IClass.STRING), sb);
        m.invokeVirtual(sb, "toString", Collections.emptyList(), IClass.STRING);
        m.returns(IClass.STRING);

        m.end();
    }

    private void implementWriteReplace() {
        /// Object writeReplace() {
        ///     return new GCAdapterSForm("loader-name", targetInterface, that);
        /// }

        IClass<GCAdapterSForm> sform = IClass.typeinfo(GCAdapterSForm.class);
        IConstructor<GCAdapterSForm> sformNew = sform.constructors().first().get();

        AsmMethodBuilder m = writer.method(IAttributes.of(IAttribute.PRIVATE), "writeReplace", Collections.emptyList(), IClass.OBJECT, Collections.emptyList());

        m.newObject(sform);
        m.dup();
        m.push(factory.loader().name());
        m.push(adapter);

        m.load(IClass.OBJECT, 0);
        m.getField(writer.type(), that, F_THAT);
        m.invokeInit(sformNew);

        m.returns(IClass.OBJECT);

        m.end();
    }

    private void implementInterface() {
        /// public Object net_ranides_assira_cg_that() {
        ///     return this.that;
        /// }

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

        m.load(IClass.OBJECT, 0);
        m.getField(writer.type(), that, F_THAT);
        m.returns(IClass.OBJECT);

        m.end();
    }

    private class MethodLink {

        private final IMethod method;
        private final IMethod target;

        private final List<IClass<?>> oargs;
        private final List<IClass<?>> iargs;

        private final int iadapter;
        private final int ireflective;

        private final AsmMethodBuilder m;

        public MethodLink(IMethod method) {
            this.method = method;
            this.target = GGAdapterLinker.find(that, method);

            this.oargs = target.arguments().types().list();
            this.iargs = method.arguments().types().list();

            this.iadapter = initResultAdapter();
            this.ireflective = initReflective();

            this.m = writer.method(IAttributes.of(IAttribute.PUBLIC), method.name(), method);
        }

        private int initResultAdapter() {
            if(!method.returns().annotation(Meta.Adapter.class).isPresent()) {
                return -1;
            }
            AdapterMeta meta = new AdapterMeta(target.returns(), method.returns());

            int found = adapters.indexOf(meta);
            if(found>=0) {
                return found;
            }

            adapters.add(meta);
            return adapters.size() - 1;
        }

        private int initReflective() {
            if(target.attribute(IAttribute.PUBLIC)) {
                return -1;
            }
            reflective.add(target.accessible());
            return reflective.size() - 1;
        }

        private void link() {
            if(iadapter != -1) {
                m.getStatic(writer.type(), IClass.typeinfo(Function.class), F_ADAPTERS+ iadapter);
            }
            if(ireflective != -1) {
                m.getStatic(writer.type(), IClass.typeinfo(IMethod.class), F_METHODS + ireflective);
            }

            m.load(IClass.OBJECT, 0);
            m.getField(writer.type(), that, F_THAT);

            if(ireflective != -1) {
                invokeReflective();
            } else {
                invokeDirect();
            }
            if (iadapter != -1) {
                m.invokeVirtual(IClass.typeinfo(Function.class), "apply", Arrays.asList(IClass.OBJECT), IClass.OBJECT);
            }
            if (!method.returns().attribute(IAttribute.VOID) && (iadapter != -1 || ireflective != -1 || !target.returns().isSubclass(method.returns()))) {
                m.checkCast(method.returns());
            }

            m.returns(method.returns());
            m.var(writer.type(), F_THIS);
            m.var(method.arguments());
            m.end();
        }

        private void invokeReflective() {
            m.push(oargs.size());
            m.newObject(IClass.typeinfo(Object[].class));

            int offset = 1;

            for (int i = 0, n = iargs.size(); i < n; i++) {
                m.dup();
                m.push(i);
                m.load(iargs.get(i), offset);
                offset += AsmSupport.sizeof(iargs.get(i));

                if(iargs.get(i).annotation(Meta.Adapter.class).isPresent()) {
                    m.invokeVirtual(M_THAT);
                }
                if (iargs.get(i).attribute(IAttribute.PRIMITIVE)) {
                    m.box(iargs.get(i));
                }
                m.storeArray(IClass.OBJECT);
            }
            m.invokeVirtual(IClass.typeinfo(IMethod.class), "invoke", Arrays.asList(IClass.OBJECT, IClass.typeinfo(Object[].class)), IClass.OBJECT);
        }

        private void invokeDirect() {
            int offset = 1;

            for(int i=0, n=iargs.size(); i<n; i++) {
                m.load(iargs.get(i), offset);
                offset += AsmSupport.sizeof(iargs.get(i));

                if(iargs.get(i).annotation(Meta.Adapter.class).isPresent()) {
                    m.invokeVirtual(M_THAT);
                }
                if(!iargs.get(i).isSubclass(oargs.get(i))) {
                    m.checkCast(oargs.get(i));
                }
            }
            m.invokeVirtual(that, target);
        }

    }

    protected Object writeReplace() {
        return SerializationUtils.proxy(this, factory, adapter, that);
    }

    @Data
    private static class AdapterMeta {

        private final IClass<?> source;
        private final IClass<?> target;

    }

}
