import java.util.Map;
import java.util.TreeMap;

public class Grammar {
    private Map<String, Definitions> grammar; // rules for all the nonterminals

    /**
     * Constructs a new, empty grammar.
     */
    public Grammar() {
        grammar = new TreeMap<String, Definitions>();
    }

    /**
     * Adds definitions for a single nonterminal to this grammar. The input
     * text should be in the form:
     * <ul><li>A single nonterminal (the thing being defined),</li>
     *     <li>The symbol "::=", and</li>
     *     <li>A list of zero or more definitions, separated by the "|" symbol.</li>
     * </ul>
     * 
     * @param ruleText The text to be parsed and kept as definitions.
     * @throws IllegalArgumentException If the input parameter has a syntax error.
     */
    public void addRule(String ruleText) throws IllegalArgumentException {
        BnfTokenizer tokenizer = new BnfTokenizer(ruleText);

        String lhs = tokenizer.nextToken();
        if (!isNonterminal(lhs)) syntaxError(ruleText);

        String token = tokenizer.nextToken();
        if (!"::=".equals(token)) syntaxError(ruleText);

        do {
            SingleDefinition singleDefinition = new SingleDefinition();
            token = tokenizer.nextToken();
            while (!token.equals("|") && !token.equals("\n") && !token.equals("EOF")) {
                singleDefinition.add(token);
                token = tokenizer.nextToken();
            }
            addToGrammar(lhs, singleDefinition);
        } while (token.equals("|"));
    }

    /**
     * Adds a single definition to this <code>Grammar</code>. If the
     * nonterminal has already been defined, the new definition is
     * appended to the existing definitions.
     * 
     * @param lhs The nonterminal being defined.
     * @param singleDefinition The new definition.
     */
    private void addToGrammar(String lhs, SingleDefinition singleDefinition) {
        Definitions fullDefinition;
        
        fullDefinition = grammar.get(lhs);
        if (fullDefinition == null) {
            fullDefinition = new Definitions();
            grammar.put(lhs, fullDefinition);
        }
        fullDefinition.add(singleDefinition);
    }

    /**
     * Throws an <code>IllegalArgumentException</code>, with the input parameter
     * as part of the exception message.
     * 
     * @param rule The text to be included in the exception.
     * @throws IllegalArgumentException To indicate a syntax error.
     */
    private void syntaxError(String rule) {
        throw new IllegalArgumentException("Syntax error in rule: " + rule);
    }
    
    /**
     * Returns a list of definitions for the given nonterminal.
     * 
     * @param nonterminal The nonterminal whose definitions are to be returned.
     * @return The definitions of the given nonterminal.
     */
    public Definitions getDefinitions(String nonterminal) {
        return grammar.get(nonterminal);
    }
    
    /**
     * Prints this Grammar.
     */
    public void print() {
        for (String lhs : grammar.keySet()) {
            Definitions fullDefinition = grammar.get(lhs);

            String oneLine = lhs + " ::= " + fullDefinition;
            if (oneLine.length() <= 72) {
                System.out.println(oneLine);
                continue;
            }
            else {
                System.out.print(lhs + " ::=");
                for (String s : fullDefinition.get(0)) {
                    System.out.print(" " + s);
                }
                System.out.println();

                String blanks = lhs.replaceAll(".", " ") + "   | ";
                for (int i = 1; i < fullDefinition.size(); i++) {
                    System.out.println(blanks + fullDefinition.get(i));
                }
            }
        }
    }

    /**
     * Returns <code>true</code> if the given string is a nonterminal,
     * as indicated by an initial <code>'&lt;'</code>.
     * @param s The token to be tested.
     * @return <code>true</code> if <code>s</code> is a nonterminal.
     */
    private static boolean isNonterminal(String s) {
        return s.startsWith("<");
    }
}