package net.ranides.assira.grammar;

import net.ranides.assira.collection.maps.CrossMap;
import net.ranides.assira.collection.maps.OpenCrossMap;
import net.ranides.assira.collection.maps.OpenMap;
import net.ranides.assira.collection.sets.OpenSet;
import net.ranides.assira.generic.Pair;
import net.ranides.assira.grammar.properties.G4NodeProperty;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class G4SelectorBuilder<V> {

    protected String name;
    protected G4Parser parser;

    protected List<G4Selector<V>> inherited = new ArrayList<>();

    protected Set<String> skipName = new OpenSet<>();
    protected Set<G4NodeProperty<?>> skipProperty1 = new OpenSet<>();
    protected Set<Pair<G4NodeProperty<?>,Object>> skipProperty2 = new OpenSet<>();
    protected Set<Predicate<G4Node>> skipIf = new OpenSet<>();

    protected Map<String, BiFunction<G4Node, G4Selector<V>, V>> mapForName = new OpenMap<>();
    protected Set<Pair<Predicate<G4Node>, BiFunction<G4Node, G4Selector<V>, V>>> mapIf = new OpenSet<>();
    protected Map<G4NodeProperty<?>, BiFunction<G4Node, G4Selector<V>, V>> mapForProperty1 = new OpenMap<>();
    protected CrossMap<G4NodeProperty<?>,Object, BiFunction<G4Node, G4Selector<V>, V>> mapForProperty2 = new OpenCrossMap<>();

    protected Map<String, BiFunction<Object, G4Selector<V>, V>> nativeMapForName = new OpenMap<>();

    public G4SelectorBuilder() {
        // do nothing
    }

    public G4SelectorBuilder<V> withName(String name) {
        this.name = name;
        return this;
    }

    public G4SelectorBuilder<V> withParser(G4Parser parser) {
        this.parser = parser;
        return this;
    }

    /**
     * ADD RULE: copy all rules from already created selector into this.
     *
     * @param selector already created selector
     * @return self
     */
    public G4SelectorBuilder<V> inherit(G4Selector<? extends V> selector) {
        this.inherited.add((G4Selector)selector);
        return this;
    }

    /**
     * ADD RULE: ignore grammar rule with specified name.
     *
     * @param ruleName name of grammar rule, as defined in G4 file
     * @return self
     */
    public G4SelectorBuilder<V> skipRule(String ruleName) {
        skipName.add(ruleName);
        return this;
    }

    /**
     * ADD RULE: ignore grammar rule with specified name.
     *
     * @param ruleName name of grammar rule, as defined in G4 file
     * @return self
     */
    public G4SelectorBuilder<V> skipRule(int ruleName) {
        validateParser();
        skipName.add(parser.ruleName(ruleName));
        return this;
    }

    /**
     * ADD RULE: ignore grammar rule with specified name.
     *
     * @param ruleName name of grammar rule, as defined in G4 file
     * @return self
     */
    public G4SelectorBuilder<V> skipRule(Class<?> ruleName) {
        validateParser();
        skipName.add(parser.ruleName(ruleName));
        return this;
    }

    /**
     * ADD RULE: skip node with specified property
     *
     * @param key property
     * @param expected value of property
     * @param <P> property type
     * @return self
     */
    public <P> G4SelectorBuilder<V> skipProperty(G4NodeProperty<P> key, P expected) {
        skipProperty2.add(Pair.of(key, expected));
        return this;
    }

    /**
     * ADD RULE: skip node with specified property
     *
     * @param key property
     * @param <P> property type
     * @return self
     */
    public <P> G4SelectorBuilder<V> skipProperty(G4NodeProperty<P> key) {
        skipProperty1.add(key);
        return this;
    }

    public <P> G4SelectorBuilder<V> skipIf(Predicate<G4Node> test) {
        skipIf.add(test);
        return this;
    }

    /**
     * ADD RULE: return specified value for grammar rule with specified name.
     *
     * @param ruleName name of grammar rule, as defined in G4 file
     * @param value constant value to return
     * @return self
     */
    public G4SelectorBuilder<V> valueForRule(String ruleName, V value) {
        return mapRule(ruleName, (node, that) -> value);
    }

    public G4SelectorBuilder<V> valueForRule(int ruleName, V value) {
        return mapRule(ruleName, (node, that) -> value);
    }

    public G4SelectorBuilder<V> valueForRule(Class<?> ruleName, V value) {
        return mapRule(ruleName, (node, that) -> value);
    }

    /**
     * ADD RULE: return specified value for token with specified name.
     *
     * @param tokenName name of token, as defined in G4 file
     * @param value constant value to return
     * @return self
     */
    public G4SelectorBuilder<V> valueForToken(String tokenName, V value) {
        return mapToken(tokenName, (node, that) -> value);
    }

    public G4SelectorBuilder<V> valueForToken(int tokenName, V value) {
        return mapToken(tokenName, (node, that) -> value);
    }

    public G4SelectorBuilder<V> valueIf(Predicate<G4Node> predicate, V value) {
        return mapIf(predicate, (node, that) -> value);
    }

    /**
     * ADD RULE: return specified value for node with specified property
     *
     * @param key property
     * @param expected value of property
     * @param value constant value to return
     * @param <P> property type
     * @return self
     */
    public <P> G4SelectorBuilder<V> valueForProperty(G4NodeProperty<P> key, P expected, V value) {
        return mapProperty(key, expected, (node, that) -> value);
    }

    /**
     * ADD RULE: return specified value for node with specified property
     *
     * @param key property
     * @param value constant value to return
     * @param <P> property type
     * @return self
     */
    public <P> G4SelectorBuilder<V> valueForProperty(G4NodeProperty<P> key, V value) {
        return mapProperty(key,(node, that) -> value);
    }

    /**
     * ADD RULE: compute value for grammar rule with specified name.
     *
     * @param ruleName name of grammar rule, as defined in G4 file
     * @param supplier expression to execute
     * @return self
     */
    public G4SelectorBuilder<V> computeForRule(String ruleName, Supplier<V> supplier) {
        return mapRule(ruleName, (node, that) -> supplier.get());
    }

    public G4SelectorBuilder<V> computeForRule(int ruleName, Supplier<V> supplier) {
        return mapRule(ruleName, (node, that) -> supplier.get());
    }

    public G4SelectorBuilder<V> computeForRule(Class<?> ruleName, Supplier<V> supplier) {
        return mapRule(ruleName, (node, that) -> supplier.get());
    }

    /**
     * ADD RULE: compute specified value for token with specified name.
     *
     * @param tokenName name of token, as defined in G4 file
     * @param supplier expression to execute
     * @return self
     */
    public G4SelectorBuilder<V> computeForToken(String tokenName, Supplier<V> supplier) {
        return mapToken(tokenName, (node, that) -> supplier.get());
    }

    public G4SelectorBuilder<V> computeForToken(int tokenName, Supplier<V> supplier) {
        return mapToken(tokenName, (node, that) -> supplier.get());
    }

    public G4SelectorBuilder<V> computeIf(Predicate<G4Node> predicate, Supplier<V> value) {
        return mapIf(predicate, (node, that) -> value.get());
    }

    /**
     * ADD RULE: return specified value for node with specified property
     *
     * @param key property
     * @param expected value of property
     * @param supplier constant value to return
     * @param <P> property type
     * @return self
     */
    public <P> G4SelectorBuilder<V> computeForProperty(G4NodeProperty<P> key, P expected, Supplier<V> supplier) {
        return mapProperty(key, expected, (node, that) -> supplier.get());
    }

    /**
     * ADD RULE: compute value for grammar rule with specified name.
     *
     * @param ruleName name of grammar rule, as defined in G4 file
     * @param function mapping expression
     * @return self
     */
    public G4SelectorBuilder<V> mapRule(String ruleName, Function<G4Node, V> function) {
        return mapRule(ruleName, (node, that) -> function.apply(node));
    }

    public G4SelectorBuilder<V> mapRule(int ruleName, Function<G4Node, V> function) {
        return mapRule(ruleName, (node, that) -> function.apply(node));
    }

    public <R> G4SelectorBuilder<V> mapRule(Class<R> ruleName, Function<R, V> function) {
        return mapRule(ruleName, (node, that) -> function.apply(node));
    }

    /**
     * ADD RULE: compute value for grammar rule with specified name.
     *
     * @param ruleName name of grammar rule, as defined in G4 file
     * @param function mapping expression
     * @return self
     */
    public G4SelectorBuilder<V> mapRule(String ruleName, BiFunction<G4Node, G4Selector<V>, V> function) {
        validateRuleName(ruleName);
        mapForName.put(ruleName, function);
        return this;
    }

    public G4SelectorBuilder<V> mapRule(int ruleName, BiFunction<G4Node, G4Selector<V>, V> function) {
        validateParser();
        mapForName.put(parser.ruleName(ruleName), function);
        return this;
    }

    public <R> G4SelectorBuilder<V> mapRule(Class<R> ruleName, BiFunction<R, G4Selector<V>, V> function) {
        validateParser();
        nativeMapForName.put(parser.ruleName(ruleName), (BiFunction)function);
        return this;
    }

    /**
     * ADD RULE: compute specified value for token with specified name.
     *
     * @param tokenName name of token, as defined in G4 file
     * @param function mapping expression
     * @return self
     */
    public G4SelectorBuilder<V> mapToken(String tokenName, Function<G4Node, V> function) {
        return mapToken(tokenName, (node, that) -> function.apply(node));
    }

    public G4SelectorBuilder<V> mapToken(int tokenName, Function<G4Node, V> function) {
        return mapToken(tokenName, (node, that) -> function.apply(node));
    }

    public G4SelectorBuilder<V> mapToken(String tokenName, BiFunction<G4Node, G4Selector<V>, V> function) {
        validateTokenName(tokenName);
        mapForName.put(tokenName, function);
        return this;
    }

    public G4SelectorBuilder<V> mapToken(int tokenName, BiFunction<G4Node, G4Selector<V>, V> function) {
        validateParser();
        mapForName.put(parser.tokenName(tokenName), function);
        return this;
    }

    public G4SelectorBuilder<V> mapIf(Predicate<G4Node> predicate, Function<G4Node, V> function) {
        return mapIf(predicate, (node, that) -> function.apply(node));
    }

    public G4SelectorBuilder<V> mapIf(Predicate<G4Node> predicate, BiFunction<G4Node, G4Selector<V>, V> function) {
        mapIf.add(Pair.of(predicate, function));
        return this;
    }

    /**
     * ADD RULE: computes value for node with specified property
     *
     * @param key property
     * @param expected value of property
     * @param function value to return
     * @param <P> property type
     * @return self
     */
    public <P> G4SelectorBuilder<V> mapProperty(
        G4NodeProperty<P> key,
        P expected,
        Function<G4Node, V> function
    ) {
        return mapProperty(key, expected, (node, that) -> function.apply(node));
    }

    public <P> G4SelectorBuilder<V> mapProperty(
        G4NodeProperty<P> key,
        Function<G4Node, V> function
    ) {
        return mapProperty(key, (node, that) -> function.apply(node));
    }

    public <P> G4SelectorBuilder<V> mapProperty(
        G4NodeProperty<P> key,
        P expected,
        BiFunction<G4Node, G4Selector<V>, V> function
    ) {
        mapForProperty2.put(key, expected, function);
        return this;
    }

    public <P> G4SelectorBuilder<V> mapProperty(
        G4NodeProperty<P> key,
        BiFunction<G4Node, G4Selector<V>, V> function
    ) {
        mapForProperty1.put(key, function);
        return this;
    }

    public G4Selector<V> prepare() {
        return new G4Selector<>(this);
    }

    private void validateParser() {
        if(parser == null) {
            throw new G4Exception(new IllegalStateException("You need to assign parser"));
        }
    }

    private void validateRuleName(String ruleName) {
        if(parser!=null && !parser.validateRuleName(ruleName)) {
            throw new G4Exception("Selector '" + name + "': invalid rule: " + ruleName);
        }
    }

    private void validateTokenName(String ruleName) {
        if(parser != null && !parser.validateTokenName(ruleName)) {
            throw new G4Exception("Selector '" + name + "': invalid token: " + ruleName);
        }
    }

}
