package cfg;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;


public class CFG {

    public static class Symbol {
        protected String value;

        public String getValue() {
            return this.value;
        }

        @Override
        public boolean equals(Object obj) {
            // Subclasses are considered not equal even if the value is the same
            if (obj == null || this.getClass() != obj.getClass()) {
                return false;
            }
            return value.equals(((Symbol) obj).value);
        }

        @Override
        public int hashCode() {
            // Subclasses should hash differently
            return Objects.hash(value, getClass());
        }

        @Override
        public String toString() {
            return this.value;
        }
    }

    public static class Terminal extends Symbol {
        public Terminal(String v) {
            // Use multiple terminals for longer strings
            assert v.length() == 1;
            this.value = v;
        }
    }

    public static class Nonterminal extends Symbol {
        public Nonterminal(String v) {
            this.value = v;
        }
    }

    public final Nonterminal start;
    private final Map<Nonterminal, List<Symbol[]>> ruleMap;

    public CFG(Nonterminal nt) {
        this.start = nt;
        this.ruleMap = new HashMap<>();
    }

    public CFG(String string) {
        this(nt(string));
    }

    public List<Symbol[]> getRules(Nonterminal symbol) {
        return Collections.unmodifiableList(ruleMap.getOrDefault(symbol, List.of()));
    }

    /**
     * Add a production to the grammar of the form lhs -> rhs
     *
     * @param lhs nonterminal on the LHS of the production
     * @param rhs Symbols on the RHS of the production, may be either terminal or nonterminal
     */
    public void addRule(Nonterminal lhs, Symbol... rhs) {
        this.ruleMap.putIfAbsent(lhs, new ArrayList<>());
        this.ruleMap.get(lhs).add(rhs);
    }

    // Various ergonomic ways of adding multiple rules for a single LHS

    /**
     * Add multiple productions for a single LHS to the grammar
     *
     * @param lhs  nonterminal on the LHS of the production
     * @param rhss Iterable of RHS's
     */
    public void addRules(Nonterminal lhs, Iterable<Symbol[]> rhss) {
        rhss.forEach(rhs -> addRule(lhs, rhs));
    }

    /**
     * Add multiple productions for a single LHS to the grammar
     *
     * @param lhs  nonterminal on the LHS of the production
     * @param rhss array of RHS's
     */
    public void addRules(Nonterminal lhs, Symbol[]... rhss) {
        Arrays.stream(rhss).forEach(rhs -> addRule(lhs, rhs));
    }

    // Shorthand constructors

    public static Terminal t(String v) {
        return new Terminal(v);
    }

    public static Nonterminal nt(String v) {
        return new Nonterminal(v);
    }
}