package net.ranides.assira.generic;

import net.ranides.assira.annotations.Meta;
import net.ranides.assira.collection.sets.OpenSet;
import net.ranides.assira.reflection.IAttribute;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.reflection.util.ClassUtils;

import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * Very naive, but hopefully correct and predictable implementation of dynamic dispatch.
 * It should select best matching overload of method.
 * Optionally, class offers double-dispatch, basing on argument and return type.
 *
 * It can be used for example to implement sophisticated casting.
 */
public class DynamicInvoker {

    private final List<RFunction> list;

    private final boolean passNull;

    private final boolean passIdent;

    private DynamicInvoker(Builder src) {
        this.list = src.map.toList();
        this.passNull = src.passNull;
        this.passIdent = src.passIdent;
    }

    /**
     * Every invoker must be constructed using builder, before use.
     * You can create new instance using "new"
     *
     * @return builder
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Executes best matching method for provided argument.
     * Best match means accepting most specific type compatible with value.
     *
     * @param value value
     * @return result
     */
    public Object call(Object value) {
        return match(value)
            .orElseThrow(() -> new IllegalArgumentException("No method found for: " + IClass.typefor(value)))
            .apply(value);
    }

    /**
     * Executes best matching method for provided argument.
     * Best match means accepting most specific type compatible with value
     * and returning type compatible with expected result.
     *
     * Strictly speaking:
     * argument is used to find closest match, but result is used to filter out invalid return types.
     *
     * @param returns returns
     * @param value value
     * @param <T> T
     * @param <R> R
     * @return result
     */
    public <T,R> R call(Class<R> returns, T value) {
        return match(returns, value)
            .orElseThrow(() -> new IllegalArgumentException("No method found for: " + IClass.typefor(value) + " =>" + ClassUtils.box(returns)))
            .apply(value);
    }

    /**
     * Executes best matching method for provided argument.
     * Best match means accepting most specific type compatible with value
     * and returning type compatible with expected result.
     *
     * Strictly speaking:
     * argument is used to find closest match, but result is used to filter out invalid return types.
     *
     * @param returns returns
     * @param value value
     * @param <T> T
     * @param <R> R
     * @return result
     */
    public <T,R> R call(IClass<R> returns, T value) {
        return match(returns, value)
            .orElseThrow(() -> new IllegalArgumentException("No method found for: " + IClass.typefor(value) + " => " + ClassUtils.box(returns)))
            .apply(value);
    }

    /**
     * Find best matching method and returns it as a "Function"
     * Best match means accepting most specific type compatible with value.
     *
     * @param value value
     * @return function
     */
    @SuppressWarnings("unchecked")
    public Ref<Function<Object, Object>> match(Object value) {
        if(value == null && passNull) {
            return Ref.of(v -> v);
        }
        for (RFunction r : list) {
            if (r.arg.isInstance(value)) {
                return Ref.of(r.func);
            }
        }
        return Ref.empty();
    }

    /**
     * Find best matching method and returns it as a "Function"
     * Best match means accepting most specific type compatible with value
     * and returning type compatible with expected result.
     *
     * Strictly speaking:
     * argument is used to find closest match, but result is used to filter out invalid return types.
     *
     * @param returns returns
     * @param value value
     * @param <T> T
     * @param <R> R
     * @return function
     */
    public <T, R> Ref<Function<T,R>> match(Class<R> returns, T value) {
        return match(IClass.typeinfo(returns), value);
    }

    /**
     * Find best matching method and returns it as a "Function"
     * Best match means accepting most specific type compatible with value
     * and returning type compatible with expected result.
     *
     * Strictly speaking:
     * argument is used to find closest match, but result is used to filter out invalid return types.
     *
     * @param returns returns
     * @param value value
     * @param <T> T
     * @param <R> R
     * @return function
     */
    @SuppressWarnings("unchecked")
    public <T, R> Ref<Function<T,R>> match(IClass<R> returns, T value) {
        if(value == null && passNull) {
            return Ref.of(v -> null);
        }
        if(value!=null && returns.isInstance(value) && passIdent) {
            return Ref.of(v -> returns.cast(v));
        }
        for (RFunction r : list) {
            if (r.arg.isInstance(value)) {
                if(returns.isSuper(r.ret)) {
                    return Ref.of(r.func);
                }

            }
        }
        if(returns.raw().isInstance(value) && passIdent) {
            return Ref.of(v -> returns.cast(v));
        }
        return Ref.empty();
    }

    /**
     * Builder pattern for DynamicInvoker
     */
    public static final class Builder {

        private final TypeList<RFunction> map = new TypeList<>();

        private boolean passNull = true;

        private boolean passIdent = true;

        /**
         * Configures invoker to return input argument without any calls, if
         * input type and return type are the same.
         *
         * @param passIdent passIdent
         * @return builder
         */
        public Builder passIdent(boolean passIdent) {
            this.passIdent = passIdent;
            return this;
        }

        /**
         * Configures invoker to return null without any calls, if input value is "null"
         *
         * @param passNull passNull
         * @return builder
         */
        public Builder passNull(boolean passNull) {
            this.passNull = passNull;
            return this;
        }

        /**
         * Inserts provided function for specified types of input and result.
         *
         * @param argument argument
         * @param returns returns
         * @param function function
         * @param <T> T
         * @param <R> R
         * @return builder
         */
        public <T,R> Builder append(Class<T> argument, Class<R> returns, Function<T, R> function) {
            return append0(argument, IClass.typeinfo(returns), function);
        }

        /**
         * Inserts provided function for specified types of input and result.
         *
         * @param argument argument
         * @param returns returns
         * @param function function
         * @param <T> T
         * @param <R> R
         * @return builder
         */
        public <T,R> Builder append(Class<T> argument, IClass<R> returns, Function<T, R> function) {
            return append0(argument, returns, function);
        }

        /**
         * Inserts provided function for specified type of input.
         * Type of result is assumed to be "void".
         *
         * @param argument argument
         * @param consumer consumer
         * @param <T> T
         * @return builder
         */
        public <T> Builder append(Class<T> argument, Consumer<T> consumer) {
            return append(argument, IClass.VOID, v -> { consumer.accept(v); return null; });
        }

        /**
         * Scans provided type for static methods annotated with "DynamicDispatch" and
         * inserts them into invoker.
         *
         * @param helper helper
         * @return builder
         */
        public Builder appendHelper(Class<?> helper) {
            IClass.typeinfo(helper).methods()
                    .require(IAttribute.STATIC)
                    .require(IAttribute.ANY)
                    .require(Meta.DynamicDispatch.class)
                    .require(1)
                    .stream()
                    .forEach(m -> {
                        Class<?> a = m.arguments().first().get().type().raw();
                        IClass<?> r = m.returns();
                        append0(a, r, v -> m.call(v));
                    });
            return this;
        }

        /**
         * Scans provided object for non-static methods annotated with "DynamicDispatch" and
         * inserts them into invoker. All methods will bind "this" to "delegate".
         *
         * @param delegate delegate
         * @return builder
         */
        public Builder appendDelegate(Object delegate) {
            IClass.typefor(delegate).methods()
                    .require(IAttribute.ANY)
                    .require(Meta.DynamicDispatch.class)
                    .require(1)
                    .stream()
                    .forEach(m -> {
                        Class<?> a = m.arguments().first().get().type().raw();
                        IClass<?> r = m.returns();

                        if (m.attributes().has(IAttribute.STATIC)) {
                            append0(a, r, v -> m.call(v));
                        } else {
                            append0(a, r, v -> m.apply(delegate, new Object[]{v}));
                        }
                    });
            return this;
        }

        private <T,R> Builder append0(Class<?> argument, IClass<?> returns, Function<T,R> function) {
            map.append(argument, new RFunction(argument, returns, function));
            return this;
        }

        /**
         * Creates new configured invoker
         *
         * @return involer
         */
        public DynamicInvoker build() {
            return new DynamicInvoker(this);
        }

    }

    private static final class RFunction {

        private final Class<?> arg;

        private final IClass<?> ret;

        private final Function func;

        public RFunction(Class<?> arg, IClass<?> ret, Function<?,?> func) {
            this.arg = ClassUtils.box(arg);
            this.ret = ret;
            this.func = func;
        }
    }

    private static class TypeList<V> {

        private final Node<V> root = new Node<>(null, null, null);

        public List<V> toList() {
            return toList(new ArrayList<>());
        }

        public List<V> toList(List<V> target) {
            toList(target, root);
            return target;
        }

        private void toList(List<V> target, Node<V> node) {
            for (Node<V> n : node.succ) {
                toList(target, n);
            }
            if(null != node.type) {
                target.addAll(node.values);
            }
        }

        public void append(Class<?> type, V value) {
            Node<V> entry = find(root, type);
            if(entry != null) {
                entry.values.add(value);
            } else {
                insertAt(root, type, value);
            }
        }

        private Node<V> find(Node<V> node, Class<?> key) {
            if (Objects.equals(node.type, key)) {
                return node;
            }

            for (Node<V> c : node.succ) {
                if (isSuccessor(c.type, key)) {
                    Node<V> ret = find(c, key);
                    if (ret != null) {
                        return ret;
                    }
                }
            }
            return null;
        }

        private boolean isSuccessor(Class<?> parent, Class<?> child) {
            return parent == null || (child != null && parent.isAssignableFrom(child));
        }

        private void insertAt(Node<V> node, Class<?> key, V value) {
            for (Node<V> c : node.succ) {
                if (isSuccessor(c.type, key)) {
                    insertAt(c, key, value);
                    return;
                } else if (isSuccessor(key, c.type)) {
                    insertAbove(c, key, value);
                    return;
                }
            }
            insertBelow(node, key, value);
        }

        private void insertBelow(Node<V> parent, Class<?> key, V value) {
            Node<V> node = new Node<>(parent, key, value);
            parent.succ.add(node);
        }

        private void insertAbove(Node<V> lower, Class<?> key, V value) {
            Node<V> upper = lower.pred;
            Node<V> node = new Node<>(upper, key, value);

            // replace relations
            node.succ.add(lower);
            lower.pred = node;

            // create new relations
            upper.succ.remove(lower);
            upper.succ.add(node);

            // fix neighbours
            Iterator<Node<V>> iterator = upper.succ.iterator();
            while (iterator.hasNext()) {
                Node<V> c = iterator.next();
                if (!node.type.equals(c.type) && isSuccessor(node.type, c.type)) {
                    iterator.remove();
                    node.succ.add(c);
                }
            }
        }

        static class Node<V> {

            Node<V> pred;

            final Class<?> type;

            final List<V> values;

            final Collection<Node<V>> succ = new OpenSet<>();

            Node(Node<V> predecessor, Class<?> type, V value) {
                this.pred = predecessor;
                this.type = type;
                this.values = new ArrayList<>(Collections.singletonList(value));
            }

        }

    }
}
