/*
 * xtc - The eXTensible Compiler
 * Copyright (C) 2004-2008 Robert Grimm
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * version 2 as published by the Free Software Foundation.
 *
 * 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
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
 * USA.
 */
package xtc.parser;

import xtc.Constants;

import xtc.util.Runtime;

import xtc.type.AST;

/**
 * Visitor to expand choices by inlining productions.  This visitor
 * inlines transient productions if they are essentially the only
 * element appearing in ordered choices' alternatives.
 *
 * <p />Note that this visitor requires that text-only productions
 * have been marked as such, that all {@link ValueElement value
 * elements} have been added to the grammar (notably, for generic
 * productions), and that the grammar's productions have been {@link
 * ReferenceCounter reference counted}.  Also note that this visitor
 * may result in new opportunities for the {@link
 * DeadProductionEliminator elimination of dead productions}.
 *
 * <p />This visitor assumes that the entire grammar is contained in a
 * single module.
 *
 * @author Robert Grimm
 * @version $Revision: 1.36 $
 */
public class ChoiceExpander extends GrammarVisitor {

  /**
   * The processing mode.  In regular mode, this visitor traverses a
   * grammar and inlines into choices; in remove values mode, it
   * removes value elements; in void values mode, it converts all
   * value elements into null value elements; and in text values mode,
   * it converts all value elements into text value elements.
   */
  public static enum Mode { 
    /** Traverse grammar and inline into choices. */ INLINE,
    /** Remove value elements. */ RM_VALUES,
    /** Convert value elements into null value elements. */ VOID_VALUES,
    /** Convert value elements into text value elements. */ TEXT_VALUES,
    /** Convert value elements into token value elements. */ TOKEN_VALUES
  }

  /** The flag for whether the grammar has the state attribute. */
  protected boolean hasState;

  /** The current processing mode. */
  protected Mode mode;

  /**
   * Create a new choice expander.
   *
   * @param runtime The runtime.
   * @param analyzer The analyzer utility.
   */
  public ChoiceExpander(Runtime runtime, Analyzer analyzer) {
    super(runtime, analyzer);
  }

  /**
   * Determine whether the specified alternative is a candidate for
   * replacement.  If the alternative is a candidate, this method
   * returns the nonterminal for inlining.  Otherwise, it returns
   * <code>null</code>.
   *
   * @param alternative The alternative.
   * @param top The flag for whether the alternative appears in a
   *   top-level choice.
   * @return The candidate nonterminal.
   */
  protected NonTerminal candidate(Sequence alternative, boolean top) {
    Element e = Analyzer.strip(alternative);

    if (e instanceof Binding) {
      // Generally, we accept a top-level alternative containing a
      // nonterminal that is bound to yyValue.
      Binding b = (Binding)e;

      if (top &&
          CodeGenerator.VALUE.equals(b.name) &&
          (b.element instanceof NonTerminal)) {
        return (NonTerminal)b.element;
      }

    } else if (AST.isVoid(analyzer.current().type) ||
               analyzer.current().getBooleanProperty(Properties.TEXT_ONLY) ||
               analyzer.current().getBooleanProperty(Properties.TOKEN)) {
      // However, for void, text-only, and token-level productions, we
      // also accept lone nonterminals or a nonterminal followed by a
      // value element.
      if (e instanceof NonTerminal) {
        return (NonTerminal)e;

      } else if (e instanceof Sequence) {
        Sequence s = (Sequence)e;

        if ((2 == s.size()) &&
            (s.get(0) instanceof NonTerminal) &&
            (s.get(1) instanceof ValueElement)) {
          return (NonTerminal)s.get(0);
        }
      }

    }

    return null;
  }

  /**
   * Record that the specified production has been inlined and, if
   * necessary, print a message to the console.
   *
   * @param p The production.
   */
  protected void inlined(Production p) {
    if (runtime.test("optionVerbose")) {
      System.err.println("[Inlining " + p.qName + " into " +
                         analyzer.current().qName + "]");
    }
  }

  /** Visit the specified grammar. */
  public Object visit(Module m) {
    // Initialize the per-grammar state.
    analyzer.register(this);
    analyzer.init(m);
    hasState = m.hasAttribute(Constants.ATT_STATEFUL.getName());
    for (Production p : m.productions) {
      mode = Mode.INLINE;

      if (runtime.test("optimizeChoices2") ||
          AST.isVoid(p.type) ||
          p.getBooleanProperty(Properties.TEXT_ONLY) ||
          p.getBooleanProperty(Properties.TOKEN)) {
        analyzer.process(p);
      }
    }

    return null;
  }

  /** Visit the specified ordered choice. */
  public Element visit(OrderedChoice c) {
    boolean top   = isTopLevel;
    isTopLevel    = false;
    boolean last  = isLastElement;
    isLastElement = false;

    for (int i=0; i<c.alternatives.size(); i++) {
      Sequence alternative = c.alternatives.get(i);

      // Only inline if we are in the correct mode.
      if (Mode.INLINE == mode) {
        NonTerminal date   = candidate(alternative, top);

        if (null != date) {
          Production p  = analyzer.lookup(date);
          MetaData   md = (MetaData)p.getProperty(Properties.META_DATA);

          // If a void, text-only, or token-level production is
          // transient/inline, is not explicit, does not reference
          // itself, and does not have the state or reset attribute,
          // inline a copy of it.  If the production is neither void,
          // text-only, nor token-level, it must have the inline
          // attribute and the choices2 optimization must be enabled.
          if ((((! p.isMemoized()) &&
                (! p.hasAttribute(Constants.ATT_NO_INLINE)) &&
                (! p.hasAttribute(Constants.ATT_EXPLICIT)) &&
                (AST.isVoid(p.type) ||
                 p.getBooleanProperty(Properties.TEXT_ONLY) ||
                 p.getBooleanProperty(Properties.TOKEN))) ||
               (p.hasAttribute(Constants.ATT_INLINE) &&
                runtime.test("optimizeChoices2"))) &&
              (0 == md.selfCount) &&
              (! (hasState &&
                  (p.hasAttribute(Constants.ATT_STATEFUL) ||
                   p.hasAttribute(Constants.ATT_RESETTING))))) {
            OrderedChoice choice = analyzer.copy(p.choice);

            // If the choice about to be inlined has only one
            // alternative (i.e., one alternative replaces another),
            // then we remember the source location of the original
            // alternative.
            if (1 == choice.alternatives.size()) {
              choice.alternatives.get(0).setLocation(alternative);
            }

            // Fix any value elements for the larger expression about
            // to be inlined.

            if ((! top) && (! last)) {
              // Embedded choices do not need any value elements.
              mode   = Mode.RM_VALUES;
              choice = (OrderedChoice)dispatch(choice);
              mode   = Mode.INLINE;

            } else if (AST.isVoid(analyzer.current().type)) {
              // Void productions need null value elements.
              mode   = Mode.VOID_VALUES;
              choice = (OrderedChoice)dispatch(choice);
              mode   = Mode.INLINE;

            } else if (analyzer.current().
                       getBooleanProperty(Properties.TEXT_ONLY) &&
                       (! top)) {
              // Embedded choices in text-only productions need text
              // value elements (but not string value elements).
              mode   = Mode.TEXT_VALUES;
              choice = (OrderedChoice)dispatch(choice);
              mode   = Mode.INLINE;
            } else if (analyzer.current().getBooleanProperty(Properties.TOKEN)) {
              // Token-level productions need token value elements.
              mode   = Mode.TOKEN_VALUES;
              choice = (OrderedChoice)dispatch(choice);
              mode   = Mode.INLINE;
            }
            c.alternatives.remove(i);
            c.alternatives.addAll(i, choice.alternatives);
            
            // Tell the user.
            inlined(p);
            
            // Process the inlined alternative(s).
            i--;
            continue;
          }
        }
      }

      // Process the unmodified alternative.
      if (top || last) isLastElement = true;
      c.alternatives.set(i, (Sequence)dispatch(alternative));
    }
    return c;
  }

  /** Visit the specified sequence. */
  public Element visit(Sequence s) {
    isTopLevel   = false;
    boolean last = isLastElement;
    int     size = s.size();

    // If we are in the remove value elements mode, check whether the
    // last element of the sequence is a value element and, if so,
    // remove it.
    if ((Mode.RM_VALUES == mode) &&
        (s.get(size-1) instanceof ValueElement)) {
      s.elements.remove(size-1);
      size--;
    }

    // Process the sequence's elements.
    for (int i=0; i<size; i++) {
      isLastElement = last && (i == size-1);
      s.elements.set(i, (Element)dispatch(s.get(i)));
    }
    isLastElement   = false;

    // Done.
    return s;
  }

  /** Visit the specified value element. */
  public Element visit(ValueElement v) {
    isTopLevel    = false;
    isLastElement = false;
    if (Mode.VOID_VALUES == mode) {
      return NullValue.VALUE;
    } else if (Mode.TEXT_VALUES == mode) {
      return StringValue.VALUE;
    } else if (Mode.TOKEN_VALUES == mode) {
      return TokenValue.VALUE;
    } else {
      return v;
    }
  }

}
