package net.ranides.assira.grammar;

import lombok.RequiredArgsConstructor;
import net.ranides.assira.collection.iterators.ForwardIterator;
import net.ranides.assira.collection.iterators.IteratorUtils;
import net.ranides.assira.collection.maps.CrossMap;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.collection.query.CQueryAbstract;
import net.ranides.assira.collection.query.support.BaseEach;
import net.ranides.assira.collection.query.support.BaseIterable;
import net.ranides.assira.collection.query.support.BaseState;
import net.ranides.assira.functional.Consumers;
import net.ranides.assira.functional.Predicates;
import net.ranides.assira.generic.Pair;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.grammar.properties.G4NodeProperty;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.Stack;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;

public class G4Selector<V> {

    // @todo review messages

    private final String name;

    private final List<G4Selector<V>> inherited;

    private final Set<String> skipName;
    private final Set<G4NodeProperty<?>> skipProperty1;
    private final Set<Pair<G4NodeProperty<?>,Object>> skipProperty2;
    private final Set<Predicate<G4Node>> skipIf;

    private final Map<String, BiFunction<G4Node, G4Selector<V>, V>> mapForName;
    private final Set<Pair<Predicate<G4Node>, BiFunction<G4Node, G4Selector<V>, V>>> mapIf;
    private final Map<G4NodeProperty<?>, BiFunction<G4Node, G4Selector<V>, V>> mapForProperty1;
    private final CrossMap<G4NodeProperty<?>,Object, BiFunction<G4Node, G4Selector<V>, V>> mapForProperty2;

    private final Map<String, BiFunction<Object, G4Selector<V>, V>> nativeMapForName;

    public G4Selector(G4SelectorBuilder<V> builder) {
        this.name = builder.name;
        this.inherited = builder.inherited;
        this.skipName = builder.skipName;
        this.skipProperty1 = builder.skipProperty1;
        this.skipProperty2 = builder.skipProperty2;
        this.skipIf = builder.skipIf;
        this.mapForName = builder.mapForName;
        this.mapIf = builder.mapIf;
        this.mapForProperty1 = builder.mapForProperty1;
        this.mapForProperty2 = builder.mapForProperty2;
        this.nativeMapForName = builder.nativeMapForName;
    }

    public String name() {
        return this.name;
    }

    public CQuery<V> stream(G4Node node) {
        return new TreeWalker(node);
    }

    public List<V> list(G4Node node) {
        return stream(node).list();
    }

    public V first(G4Node node) {
        return stream(node).first().get();
    }

    public V single(G4Node node) {
        return stream(node).single().get();
    }

    public Ref<V> optional(G4Node node) {
        return stream(node).first();
    }

    public G4NodeSelectorBranch bind() {
        return new G4NodeSelectorBranch();
    }

    public CQuery<String> coverage() {
        return CQuery.builder()
            .withCollection(skipName)
            .withCollection(mapForName.keySet())
            .withCollection(nativeMapForName.keySet())
            .build();
    }

    private boolean shouldSkip(G4Node node) {
        for(G4Selector<V> s : inherited) {
            if(s.shouldSkip(node)) {
                return true;
            }
        }
        if(skipName.contains(node.shortname()) || skipName.contains(node.qualifier()) || skipName.contains(node.name())) {
            return true;
        }

        for(G4NodeProperty<?> p : skipProperty1) {
            if(node.has(p)) {
                return true;
            }
        }

        for(Pair<G4NodeProperty<?>,Object> pair : skipProperty2) {
            if(node.has(pair.getFirst(), pair.getSecond())) {
                return true;
            }
        }

        for(Predicate<G4Node> p : skipIf) {
            if(p.test(node)) {
                return true;
            }
        }

        return false;
    }

    private Ref<V> tryMatch(G4Node node) {
        for(G4Selector<V> s : inherited) {
            Ref<V> vs = s.tryMatch(node);
            if(vs.isPresent()) {
                return vs;
            }
        }

        if(mapForName.containsKey(node.shortname())) {
            return Ref.of(mapForName.get(node.shortname()).apply(node, this));
        }
        if(mapForName.containsKey(node.qualifier())) {
            return Ref.of(mapForName.get(node.qualifier()).apply(node, this));
        }
        if(mapForName.containsKey(node.name())) {
            return Ref.of(mapForName.get(node.name()).apply(node, this));
        }
        if(nativeMapForName.containsKey(node.shortname())) {
            return Ref.of(nativeMapForName.get(node.shortname()).apply(node.unwrap(), this));
        }
        if(nativeMapForName.containsKey(node.qualifier())) {
            return Ref.of(nativeMapForName.get(node.qualifier()).apply(node.unwrap(), this));
        }
        if(nativeMapForName.containsKey(node.name())) {
            return Ref.of(nativeMapForName.get(node.name()).apply(node.unwrap(), this));
        }

        for (Map.Entry<G4NodeProperty<?>, BiFunction<G4Node, G4Selector<V>, V>> entry : mapForProperty1.entrySet()) {
            G4NodeProperty<?> p = entry.getKey();
            BiFunction<G4Node, G4Selector<V>, V> m = entry.getValue();
            if(node.has(p)) {
                return Ref.of(m.apply(node, this));
            }
        }

        for (Map.Entry<Pair<G4NodeProperty<?>, Object>, BiFunction<G4Node, G4Selector<V>, V>> entry : mapForProperty2.entrySet()) {
            G4NodeProperty<?> p = entry.getKey().getFirst();
            Object v = entry.getKey().getSecond();
            BiFunction<G4Node, G4Selector<V>, V> m = entry.getValue();
            if(node.has(p, v)) {
                return Ref.of(m.apply(node, this));
            }
        }

        for (Pair<Predicate<G4Node>, BiFunction<G4Node, G4Selector<V>, V>> pair : mapIf) {
            Predicate<G4Node> p = pair.getFirst();
            BiFunction<G4Node, G4Selector<V>, V> m = pair.getSecond();
            if(p.test(node)) {
                return Ref.of(m.apply(node, this));
            }
        }

        return Ref.empty();
    }

    public class G4NodeSelectorBranch {

        private final List<Ref<G4Node>> source;

        public G4NodeSelectorBranch() {
            this.source = new ArrayList<>();
        }

        public G4NodeSelectorBranch input(G4Node node) {
            source.add(Ref.ofNullable(node));
            return this;
        }

        public G4NodeSelectorBranch input(Optional<G4Node> node) {
            source.add(Ref.ofOptional(node));
            return this;
        }

        public G4NodeSelectorBranch input(Ref<G4Node> node) {
            source.add(node);
            return this;
        }

        public String name() {
            return name;
        }

        public CQuery<V> stream() {
            return CQuery.from().list(source).mapOptional(v -> v).flat(v -> G4Selector.this.stream(v));
        }

        public List<V> list() {
            return stream().list();
        }

        public V first() {
            return stream().first().get();
        }

        public V single() {
            return stream().single().get();
        }

        public Ref<V> optional() {
            return stream().first();
        }
    }

    @RequiredArgsConstructor
    public class TreeWalker extends CQueryAbstract<V> {

        private final G4Node root;

        @Override
        public Supplier<? extends RuntimeException> message() {
            return () -> new NoSuchElementException("Selector '" + name +"' not found results for '" + root + "'");
        }

        @Override
        public CQuery<V> message(Supplier<? extends RuntimeException> message) {
            return this;
        }

        @Override
        public CQuery<V> parallel() {
            return this;
        }

        @Override
        public CQuery<V> sequential() {
            return this;
        }

        @Override
        public boolean isParallel() {
            return false;
        }

        @Override
        public boolean hasFastEach() {
            return true;
        }

        @Override
        public Stream<V> stream() {
            return BaseIterable.stream(this);
        }

        @Override
        public Iterator<V> iterator() {
            return IteratorUtils.withCause(new TreeIterator(root), message());
        }

        @Override
        public void forEach(Consumer<? super V> consumer) {
            forEach0(root, consumer);
        }

        @Override
        public void forEach(Consumers.EachConsumer<? super V> consumer) {
            BaseState.Index index = BaseState.newIndex(this);
            forEach0(root, v -> consumer.accept(index.next(), v));
        }

        @Override
        public boolean whileEach(Predicate<? super V> consumer) {
            return whileEach0(root, consumer);
        }

        @Override
        public boolean whileEach(Predicates.EachPredicate<? super V> consumer) {
            BaseState.Index index = BaseState.newIndex(this);
            return whileEach0(root, v -> consumer.test(index.next(), v) );
        }

        private void forEach0(G4Node node, Consumer<? super V> consumer) {
            if(shouldSkip(node)) {
                return;
            }
            Ref<V> out = tryMatch(node);
            if(out.isPresent()) {
                consumer.accept(out.get());
            } else {
                node.children().forEach(n -> forEach0(n, consumer));
            }
        }

        private boolean whileEach0(G4Node node, Predicate<? super V> consumer) {
            if(shouldSkip(node)) {
                return false;
            }
            Ref<V> out = tryMatch(node);
            if(out.isPresent()) {
                return consumer.test(out.get());
            } else {
                return node.children().whileEach(n -> whileEach0(n, consumer));
            }
        }

        @Override
        public int size() {
            return BaseEach.size(this);
        }

        private final class TreeIterator extends ForwardIterator<V> {

            private final Stack<Iterator<G4Node>> window;

            public TreeIterator(G4Node node) {
                this.window = new Stack<>();
                this.window.add(Collections.singletonList(node).iterator());
            }

            @Override
            protected boolean next(Consumer<? super V> action) {
                while (!window.isEmpty()) {
                    Iterator<G4Node> nodes = window.peek();
                    if (nodes.hasNext()) {
                        G4Node node = nodes.next();
                        if(!shouldSkip(node)) {
                            // try match
                            Ref<V> out = tryMatch(node);
                            if(out.isPresent()) {
                                action.accept(out.get());
                                return true;
                            } else {
                                window.push(node.children().iterator());
                                return next(action);
                            }
                        }
                    } else {
                        window.pop();
                    }
                }
                return false;
            }
        }

    }

}
