package net.ranides.assira.reflection.impl;

import net.ranides.assira.collection.maps.OpenMap;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.reflection.IArguments;
import net.ranides.assira.reflection.IAttribute;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.reflection.IMethod;

import java.util.List;
import java.util.Map;

public class GGAdapterLinker {

    private final IMethod method;

    private final CQuery<IMethod> prv;

    private final CQuery<IMethod> pub;

    /**
     * Takes method from "adapter" class returns best matching method from "that" class.
     * <p>
     * Returned method can have incompatible signature and could require additional bytecode for
     * conversion. Details about conversions will be handled by CGAdapter.MethodLink
     *
     * @param that   original class
     * @param method adapter method
     * @return that method
     */
    public static IMethod find(IClass<?> that, IMethod method) {
        return new GGAdapterLinker(that, method).find();
    }

    private GGAdapterLinker(IClass<?> type, IMethod method) {
        this.method = method;

        this.pub = type.methods().require(IAttribute.PUBLIC).require(method.name()).stream().fetch();

        Map<IArguments, IMethod> map = new OpenMap<>();
        type
            .methods()
            .require(IAttribute.ANY)
            .discard(IAttribute.BRIDGE)
            .discard(IAttribute.SYNTHETIC)
            .discard(IAttribute.PUBLIC)
            .require(method.name())
            .each(m -> map.put(m.arguments(), m));

        this.prv = CQuery.from().collection(map.values());
    }

    private IMethod find() {
        Ref<IMethod> out;

        if ((out = findFast()).isPresent()) {
            return out.get();
        }

        if ((out = findExact(pub)).isPresent()) {
            return out.get();
        }
        if ((out = findExact(prv)).isPresent()) {
            return out.get();
        }

        if ((out = findAssignable(pub)).isPresent()) {
            return out.get();
        }
        if ((out = findAssignable(prv)).isPresent()) {
            return out.get();
        }

        if ((out = findNarrowing(pub)).isPresent()) {
            return out.get();
        }
        if ((out = findPossible(pub)).isPresent()) {
            return out.get();
        }
        if ((out = findNarrowing(prv)).isPresent()) {
            return out.get();
        }
        if ((out = findPossible(prv)).isPresent()) {
            return out.get();
        }

        throw new LinkageError("No compatible binding to " + method);
    }

    private Ref<IMethod> findFast() {
        if (pub.size() == 1 && prv.isEmpty()) {
            Ref<IMethod> found = pub.first();
            if (found.isPresent() && !isPossible(method, found.get())) {
                throw new LinkageError("No compatible binding to " + method);
            }
            return found;
        }
        return Ref.empty();
    }

    private Ref<IMethod> findExact(CQuery<IMethod> query) {
        return query.filter(r -> r.declares(method.arguments())).first();
    }

    private Ref<IMethod> findAssignable(CQuery<IMethod> query) {
        return query
            .filter(r -> r.matches(method.arguments()))
            .single(() -> new LinkageError("Ambiguous widening binding to " + method));
    }

    private Ref<IMethod> findNarrowing(CQuery<IMethod> query) {
        return query
            .filter(r -> method.matches(r.arguments()))
            .single(() -> new LinkageError("Ambiguous narrowing binding to " + method));
    }

    private Ref<IMethod> findPossible(CQuery<IMethod> query) {
        return query
            .filter(r -> isPossible(method, r))
            .single(() -> new LinkageError("Ambiguous possible binding to " + method));
    }

    private boolean isPossible(IMethod m1, IMethod m2) {
        if (m1.arguments().count() != m2.arguments().count()) {
            return false;
        }
        List<IClass<?>> args1 = m1.arguments().types().list();
        List<IClass<?>> args2 = m2.arguments().types().list();
        for (int i = 0, n = args1.size(); i < n; i++) {
            if (!isPossible(args1.get(i), args2.get(i))) {
                return false;
            }
        }
        return true;
    }

    private boolean isPossible(IClass<?> c1, IClass<?> c2) {
        return (!c1.attribute(IAttribute.PRIMITIVE) && !c2.attribute(IAttribute.PRIMITIVE)) || c1.isEquivalent(c2);
    }
}
