/*
 * SecondPassAnalyzer.java
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the BSD license.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * LICENSE.txt file for more details.
 *
 * Copyright (c) 2003-2015 Per Cederberg. All rights reserved.
 */

package net.percederberg.grammatica;

import net.percederberg.grammatica.parser.Node;
import net.percederberg.grammatica.parser.ParseException;
import net.percederberg.grammatica.parser.ParserCreationException;
import net.percederberg.grammatica.parser.Production;
import net.percederberg.grammatica.parser.ProductionPattern;
import net.percederberg.grammatica.parser.ProductionPatternAlternative;
import net.percederberg.grammatica.parser.ProductionPatternElement;
import net.percederberg.grammatica.parser.Token;
import net.percederberg.grammatica.parser.TokenPattern;

/**
 * A second pass grammar analyzer. This class processes the grammar
 * parse tree and adds all production pattern rules to the patterns.
 * All required syntetic production patterns will also be added to
 * the grammar.
 *
 * @author   Per Cederberg
 * @version  1.0
 */
class SecondPassAnalyzer extends GrammarAnalyzer {

    /**
     * The grammar where tokens and patterns are stored.
     */
    private Grammar grammar;

    /**
     * The current production pattern. This is set when processing the
     * production declaration, and is used when creating syntetic
     * productions.
     */
    private ProductionPattern currentProduction = null;

    /**
     * The next free syntetic production id.
     */
    private int nextSynteticId = 3001;

    /**
     * Creates a new grammar analyser.
     *
     * @param grammar        the grammar where objects are added
     */
    public SecondPassAnalyzer(Grammar grammar) {
        this.grammar = grammar;
    }

    /**
     * Sets the node value to the token or production pattern. If no
     * matching pattern was found, an exception is thrown.
     *
     * @param node           the token node
     *
     * @return the token node
     *
     * @throws ParseException if the node analysis discovered errors
     */
    protected Node exitIdentifier(Token node) throws ParseException {
        String             name = node.getImage();
        TokenPattern       token = grammar.getTokenPatternByName(name);
        ProductionPattern  prod = grammar.getProductionPatternByName(name);

        if (token != null) {
            node.addValue(token);
        } else if (prod != null) {
            node.addValue(prod);
        } else {
            throw new ParseException(
                ParseException.ANALYSIS_ERROR,
                "unrecognized identifier '" + name + "'",
                node.getStartLine(),
                node.getStartColumn());
        }
        return node;
    }

    /**
     * Sets the node value to the token pattern. If no matching
     * pattern was found, an exception is thrown.
     *
     * @param node           the token node
     *
     * @return the token node
     *
     * @throws ParseException if the node analysis discovered errors
     */
    protected Node exitQuotedString(Token node) throws ParseException {
        String        str;
        TokenPattern  token;

        str = node.getImage();
        str = str.substring(1, str.length() - 1);
        token = grammar.getTokenPatternByImage(str);
        if (token != null) {
            node.addValue(token);
        } else {
            throw new ParseException(
                ParseException.ANALYSIS_ERROR,
                "unrecognized token \"" + str + "\"",
                node.getStartLine(),
                node.getStartColumn());
        }
        return node;
    }

    /**
     * Removes the parse tree by returning null.
     *
     * @param node           the production node
     *
     * @return the new production node
     */
    protected Node exitGrammar(Production node) {
        return null;
    }

    /**
     * Removes the production part from the parse tree by returning
     * null.
     *
     * @param node           the production node
     *
     * @return the new production node
     */
    protected Node exitProductionPart(Production node) {
        return null;
    }

    /**
     * Sets the production name variable when encountering the
     * identifier child. This variable is used when creating new
     * subproductions.
     *
     * @param node           the production node
     * @param child          the child to add
     *
     * @throws ParseException if the node analysis discovered errors
     */
    protected void childProductionDeclaration(Production node, Node child)
        throws ParseException {

        super.childProductionDeclaration(node, child);
        if (child.getId() == GrammarConstants.IDENTIFIER) {
            currentProduction = (ProductionPattern) child.getValue(0);
        }
    }

    /**
     * Adds all the pattern rules to the production pattern. This
     * method also removes the production declaration from the parse
     * tree by returning null.
     *
     * @param node           the production node
     *
     * @return the new production node
     *
     * @throws ParseException if the node analysis discovered errors
     */
    protected Node exitProductionDeclaration(Production node)
        throws ParseException {

        ProductionPattern             pattern;
        ProductionPatternAlternative  alt;
        Node                          child;

        pattern = (ProductionPattern) getValue(getChildAt(node, 0), 0);
        child = getChildAt(node, 2);
        for (int i = 0; i < child.getValueCount(); i++) {
            alt = (ProductionPatternAlternative) getValue(child, i);
            try {
                pattern.addAlternative(alt);
            } catch (ParserCreationException e) {
                throw new ParseException(
                    ParseException.ANALYSIS_ERROR,
                    e.getMessage(),
                    node.getStartLine(),
                    node.getStartColumn());
            }
        }
        return null;
    }

    /**
     * Sets the node values to the pattern rules.
     *
     * @param node           the production node
     *
     * @return the new production node
     *
     * @throws ParseException if the node analysis discovered errors
     */
    protected Node exitProduction(Production node) throws ParseException {
        ProductionPatternAlternative  alt;
        ProductionPatternElement      elem;
        Node                          child;

        alt = new ProductionPatternAlternative();
        node.addValue(alt);
        for (int i = 0; i < node.getChildCount(); i++) {
            child = getChildAt(node, i);
            if (child.getId() == GrammarConstants.PRODUCTION_ATOM) {
                for (int j = 0; j < child.getValueCount(); j++) {
                    elem = (ProductionPatternElement) getValue(child, j);
                    alt.addElement(elem);
                }
            } else if (child.getId() == GrammarConstants.PRODUCTION) {
                node.addValues(child.getAllValues());
            }
        }

        return node;
    }

    /**
     * Sets the node value to the production pattern element.
     *
     * @param node           the production node
     *
     * @return the new production node
     *
     * @throws ParseException if the node analysis discovered errors
     */
    protected Node exitProductionAtom(Production node)
        throws ParseException {

        Node     child;
        boolean  token = false;
        int      id = 0;
        int      min = 1;
        int      max = 1;
        Object   obj;

        // Handle the alternatives
        child = getChildAt(node, 0);
        switch (child.getId()) {
        case GrammarConstants.IDENTIFIER:
            obj = getValue(child, 0);
            if (obj instanceof TokenPattern) {
                token = true;
                id = ((TokenPattern) obj).getId();
            } else {
                token = false;
                id = ((ProductionPattern) obj).getId();
            }
            break;
        case GrammarConstants.QUOTED_STRING:
            token = true;
            id = ((TokenPattern) getValue(child, 0)).getId();
            break;
        case GrammarConstants.LEFT_PAREN:
        case GrammarConstants.LEFT_BRACE:
        case GrammarConstants.LEFT_BRACKET:
            ProductionPatternElement  elem;

            if (child.getId() == GrammarConstants.LEFT_BRACE) {
                min = 0;
                max = -1;
            } else if (child.getId() == GrammarConstants.LEFT_BRACKET) {
                min = 0;
                max = 1;
            }
            elem = getProductionElement(getChildAt(node, 1));
            token = elem.isToken();
            id = elem.getId();
            break;
        }

        // Handle optional '?', '*' or '+'
        child = getChildAt(node, node.getChildCount() - 1);
        if (child.getId() == GrammarConstants.QUESTION_MARK) {
            min = 0;
            max = 1;
        } else if (child.getId() == GrammarConstants.ASTERISK) {
            min = 0;
            max = -1;
        } else if (child.getId() == GrammarConstants.PLUS_SIGN) {
            min = 1;
            max = -1;
        }

        // Create production pattern element
        node.addValue(new ProductionPatternElement(token, id, min, max));
        return node;
    }

    /**
     * Returns the production pattern element for a production node.
     * The production node only contains a set of production rules, so
     * this method normally creates a syntetic production and adds all
     * the rules to it. If only a single rule was present in the rule
     * set, and if it contained only an single mandatory element, that
     * element will be returned instead.
     *
     * @param node           the production parse tree node
     *
     * @return the production pattern element
     *
     * @throws ParseException if the node analysis discovered errors
     */
    private ProductionPatternElement getProductionElement(Node node)
        throws ParseException {

        ProductionPattern             prod;
        ProductionPatternAlternative  alt;
        String                        str;

        alt = (ProductionPatternAlternative) getValue(node, 0);
        if (node.getValueCount() == 1 && isSimple(alt)) {
            return alt.getElement(0);
        } else {
            str = currentProduction.getName() + "(" +
                  (nextSynteticId - 3000) + ")";
            prod = new ProductionPattern(nextSynteticId, str);
            prod.setSynthetic(true);
            for (int i = 0; i < node.getValueCount(); i++) {
                alt = (ProductionPatternAlternative) getValue(node, i);
                try {
                    prod.addAlternative(alt);
                } catch (ParserCreationException e) {
                    throw new ParseException(
                        ParseException.ANALYSIS_ERROR,
                        e.getMessage(),
                        node.getStartLine(),
                        node.getStartColumn());
                }
            }
            grammar.addProduction(prod,
                                  node.getStartLine(),
                                  node.getEndLine());
            return new ProductionPatternElement(false,
                                                nextSynteticId++,
                                                1,
                                                1);
        }
    }

    /**
     * Checks if a production pattern alternative contains a single
     * mandatory element.
     *
     * @param alt            the production pattern alternative
     *
     * @return true if the alternative is simple, or
     *         false otherwise
     */
    private boolean isSimple(ProductionPatternAlternative alt) {
        return alt.getElementCount() == 1
            && alt.getMinElementCount() == 1
            && alt.getMaxElementCount() == 1;
    }
}
