package recognizer;

import tokenizer.Token;
import tokenizer.TokenType;
import tokenizer.Tokenizer;

/**
 * This class consists of a number of methods that "recognize" strings
 * composed of Tokens that follow the indicated grammar rules for each
 * method.
 * <p>Each method may have one of three outcomes:
 * <ul>
 *   <li>The method may succeed, returning <code>true</code> and
 *      consuming the tokens that make up that particular nonterminal.</li>
 *   <li>The method may fail, returning <code>false</code> and not
 *       consuming any tokens.</li>
 *   <li>(Some methods only) The method may determine that an
 *       unrecoverable error has occurred and throw a
 *       <code>SyntaxException</code></li>.
 * </ul>
 * @author David Matuszek
 * @version February 21, 2012
 */
public class Recognizer {
    Tokenizer tokenizer = null;

    /**
     * Constructs a Recognizer for the given string.
     * @param text The string to be recognized.
     */
    public Recognizer(String text) {
        tokenizer = new Tokenizer(text);
    }

    /**
     * Returns the Tokenizer being used; needed for testing.
     * @return The current Tokenizer.
     */
    public Tokenizer getTokenizer() {
        return tokenizer;
    }

    /**
     * Tries to recognize an &lt;expression&gt;.
     * <pre>&lt;expression&gt; ::= [ &lt;add_operator&gt; ] &lt;term&gt; { &lt;add_operator&gt; &lt;term&gt; }</pre>
     * A <code>SyntaxException</code> will be thrown if an &lt;add_operator&gt;
     * is present after the first &lt;term&gt; but not followed by a valid &lt;expression&gt;.
     * @return <code>true</code> if an &lt;expression&gt; is recognized.
     */
    public boolean expression() {
        boolean prefix = addOperator(); // handles unary + and unary -
        if (!term()) return false;
        while (addOperator()) {
            if (!term()) {
                error("Error in arithmetic expression after '+' or '-'");
            }
        }
        return true;
    }

    /**
     * Tries to recognize a &lt;term&gt;.
     * <pre>&lt;term&gt; ::= &lt;factor&gt; { &lt;multiply_operator&gt; &lt;factor&gt; }</pre>
     * A <code>SyntaxException</code> will be thrown if the &lt;multiply_operator&gt;
     * is present but not followed by a valid &lt;term&gt;.
     * @return <code>true</code> if a term is recognized.
     */
    public boolean term() {
        if (!factor()) return false;
        while (multiplyOperator()) {
            if (!factor()) error("No term after '+' or '-'");
        }
        return true;
    }

    /**
     * Tries to recognize a &lt;factor&gt;.
     * <pre>&lt;factor&gt; ::= &lt;name&gt;
     *           | &lt;number&gt;
     *           | "x"
     *           | "y"
     *           | "(" &lt;expression&gt; ")"</pre>
     * A <code>SyntaxException</code> will be thrown if the opening
     * parenthesis is present but not followed by a valid
     * &lt;expression&gt; and a closing parenthesis.
     * @return <code>true</code> if a factor is recognized.
     */
    public boolean factor() {
        if (variable()) {
            return true;
        }
        if (isNumber()) {
            return true;
        }
        if (isKeyword("x") || isKeyword("y")) {
            return true;
        }
        if (isGroupingSymbol("(")) {
            if (!expression()) error("Error in parenthesized expression");
            if (!isGroupingSymbol(")")) error("Unclosed parenthetical expression");
            return true;
       }
       return false;
    }

    /**
     * Tries to recognize an &lt;add_operator&gt;.
     * <pre>&lt;add_operator&gt; ::= "+" | "-"</pre>
     * @return <code>true</code> if an &lt;add_operator&gt; is recognized.
     */
    public boolean addOperator() {
        return isOperator("+") || isOperator("-");
    }

    /**
     * Tries to recognize a &lt;multiply_operator&gt;.
     * <pre>&lt;multiply_operator&gt; ::= "*" | "/"</pre>
     * @return <code>true</code> if a &lt;multiply_operator&gt; is recognized.
     */
    public boolean multiplyOperator() {
        return isOperator("*") || isOperator("/");
    }

    /**
     * Tries to recognize a &lt;variable&gt;.
     * <pre>&lt;variable&gt; ::= &lt;NAME&gt;</pre>
     * @return <code>true</code> if a &lt;variable&gt; is recognized.
     */
    public boolean variable() {
        return isName();
    }



//----- Private "helper" methods

    /**
   * Tests whether the next token is a number (either integer or
   * floating point). If it is, the token is consumed, otherwise
   * it is not.
   *
   * @return <code>true</code> if the next token is a number.
   */
      private boolean isNumber() {
        return nextTokenMatches(TokenType.INT);
    }

    /**
     * Tests whether the next token is a name. If it is, the token
     * is consumed, otherwise it is not.
     *
     * @return <code>true</code> if the next token is a name.
     */
    private boolean isName() {
        return nextTokenMatches(TokenType.NAME);
    }

    /**
     * Tests whether the next token is the expected name. If it is, the token
     * is consumed, otherwise it is not.
     *
     * @param expectedName The String value of the expected next token.
     * @return <code>true</code> if the next token is a name with the expected value.
     */
    private boolean isName(String expectedName) {
        return nextTokenMatches(TokenType.NAME, expectedName);
    }

    /**
     * Tests whether the next token is the expected keyword. If it is, the token
     * is consumed, otherwise it is not.
     *
     * @param expectedKeyword The String value of the expected next token.
     * @return <code>true</code> if the next token is a keyword with the expected value.
     */
    private boolean isKeyword(String expectedKeyword) {
        return nextTokenMatches(TokenType.KEYWORD, expectedKeyword);
    }

    /**
     * Tests whether the next token is the expected symbol. If it is,
     * the token is consumed, otherwise it is not.
     *
     * @param expectedOperator The String value of the token we expect
     *    to encounter next.
     * @return <code>true</code> if the next token is the expected symbol.
     */
    boolean isOperator(String expectedOperator) {
        return nextTokenMatches(TokenType.OPERATOR, expectedOperator);
    }

    /**
     * Tests whether the next token is the expected grouping symbol. If it is,
     * the token is consumed, otherwise it is not.
     *
     * @param expectedSymbol The String value of the token we expect
     *    to encounter next.
     * @return <code>true</code> if the next token is the expected symbol.
     */
    boolean isGroupingSymbol(String expectedSymbol) {
        return nextTokenMatches(TokenType.GROUPING_SYMBOL, expectedSymbol);
    }
    /**
     * Tests whether the next token has the expected type. If it does,
     * the token is consumed, otherwise it is not. This method would
     * normally be used only when the token's value is not relevant.
     *
     * @param type The expected type of the next token.
     * @return <code>true</code> if the next token has the expected type.
     */
    public boolean nextTokenMatches(TokenType type) {
        Token t = tokenizer.next();
        if (t.type == type) return true;
        tokenizer.backUp();
        return false;
    }

    /**
     * Tests whether the next token has the expected type and value.
     * If it does, the token is consumed, otherwise it is not. This
     * method would normally be used when the token's value is
     * important.
     *
     * @param type The expected type of the next token.
     * @param text The expected text of the next token; must
     *              not be <code>null</code>.
     * @return <code>true</code> if the next token has the expected type.
     */
    public boolean nextTokenMatches(TokenType type, String text) {
        Token t = tokenizer.next();
        if (type == t.type && text.equals(t.text)) {
            return true;
        }
        tokenizer.backUp();
        return false;
    }

    /**
     * Utility routine to throw a <code>RuntimeException</code> with the
     * given message.
     * @param message The text to put in the <code>RuntimeException</code>.
     */
    private static void error(String message) {
        throw new RuntimeException(message);
    }
}
