/*
 * xtc - The eXTensible Compiler
 * Copyright (C) 2006-2007 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 java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import xtc.Constants;

import xtc.tree.Printer;
import xtc.tree.Visitor;

import xtc.type.AST;
import xtc.type.Type;
import xtc.type.TupleT;
import xtc.type.UnitT;
import xtc.type.VariantT;

import xtc.util.Runtime;

/**
 * Visitor to determine a grammar's tree structure.  This visitor
 * assumes that the entire grammar is contained in a single module and
 * has been annotated with appropriate value elements.
 *
 * @author Robert Grimm
 * @version $Revision: 1.26 $
 */
public class TreeTyper extends Visitor {

  /** The debug flag. */
  private static final boolean DEBUG = false;

  /** The runtime. */
  protected final Runtime runtime;

  /** The analyzer utility. */
  protected final Analyzer analyzer;

  /** The common type operations. */
  protected final AST ast;

  /** The flag for strict unification. */
  protected boolean strict;

  /** The flag for flattening lists. */
  protected boolean flatten;

  /** The current sequence. */
  protected Sequence sequence;

  /**
   * Create a new tree typer.
   *
   * @param runtime The runtime.
   * @param ast The type operations.
   * @param analyzer The analyzer utility.
   */
  public TreeTyper(Runtime runtime, Analyzer analyzer, AST ast) {
    this.runtime  = runtime;
    this.analyzer = analyzer;
    this.ast      = ast;
  }

  /** Visit the specified module. */
  public void visit(Module m) {
    // Initialize the per-grammar state.
    analyzer.register(this);
    analyzer.init(m);

    strict  = runtime.test("optionVariant");
    flatten = m.hasAttribute(Constants.ATT_FLATTEN);

    // Process the productions.
    for (Production p : m.productions) analyzer.process(p);

    // Print the header.
    final Printer printer = runtime.console();

    printer.sep();
    printer.indent().p("// Generated by Rats!, version ").p(Constants.VERSION).
      p(", ").p(Constants.COPY).pln('.');
    printer.sep();
    printer.pln();

    printer.indent().p("/** AST structure for grammar ").p(m.name.name).
      pln(". */");

    // Ensure that all tuples are concrete and then print the result.
    if (! runtime.test("optionVariant")) {
      // Flat AST definition.
      final VariantT node = ast.toVariant("Node", false);

      ast.concretizeTuples(node, UnitT.TYPE);
      printer.indent().p("module ").p(m.name.name).p("Tree").pln(';');
      printer.pln();
      ast.print(node, printer, true, false, null);
      printer.pln();

    } else {
      // Hierarchical AST definition.
      final VariantT     root      =
        analyzer.lookup((NonTerminal)m.getProperty(Properties.ROOT)).
        type.resolve().toVariant();
      final AST.MetaData meta      = ast.getMetaData(root);
      final Set<String>  processed = new HashSet<String>();

      // Concretize variants.
      for (Production p : m.productions) {
        if (AST.isStaticNode(p.type)) {
          final VariantT variant = p.type.resolve().toVariant();
          final String   name    = variant.getName();

          if (meta.reachable.contains(name) && ! processed.contains(name)) {
            processed.add(name);
            ast.concretizeTuples(variant, UnitT.TYPE);
          }
        }
      }
      processed.clear();

      // Print variants...
      if (! meta.modularize) {
        // ... in a single module.
        printer.indent().p("module ").p(m.name.name).p("Tree").pln(';');
        printer.pln();

        for (Production p : m.productions) {
          if (AST.isStaticNode(p.type)) {
            final VariantT variant = p.type.resolve().toVariant();
            final String   name    = variant.getName();
            
            if (meta.reachable.contains(name) && ! processed.contains(name)) {
              processed.add(name);
              ast.print(variant, printer, true, false, null);
              printer.pln();
            }
          }
        }

      } else {
        // ... across several modules.
        boolean first = true;
        String  module;

        do {
          module = null;

          for (Production p : m.productions) {
            if (AST.isStaticNode(p.type)) {
              final VariantT variant = p.type.resolve().toVariant();
              final String   name    = variant.getName();

              if (meta.reachable.contains(name) && ! processed.contains(name)) {
                final String qualifier = variant.getQualifier();
                
                if (null == module) {
                  module = qualifier;
                  
                  if (first) {
                    first = false;
                  } else {
                    printer.sep().pln();
                  }
                  printer.indent().p("module ").p(module).pln(';');
                  printer.pln();
                  
                } else if (! module.equals(qualifier)) {
                  continue;
                }
                
                processed.add(name);
                ast.print(variant, printer, true, true, module);
                printer.pln();
              }
            }
          }
        } while (null != module);
      }
    }

    printer.sep().flush();
  }

  /** Visit the specified production. */
  public void visit(Production p) {
    dispatch(p.choice);
  }

  /** Visit the specified ordered choice. */
  public void visit(OrderedChoice c) {
    for (Sequence alt : c.alternatives) dispatch(alt);
  }

  /** Visit the specified sequence. */
  public void visit(Sequence s) {
    final Sequence old = sequence;
    sequence           = s;

    for (Element e : s.elements) dispatch(e);

    sequence           = old;
  }

  /** Visit the specified unary operator. */
  public void visit(UnaryOperator op) {
    dispatch(op.element);
  }

  /** Visit the specified element. */
  public void visit(Element e) {
    // Nothing to do.
  }

  /** Visit the specified generic node value. */
  public void visit(GenericNodeValue v) {
    process(v.name, false, v.children);
  }

  /** Visit the specified generic action value. */
  public void visit(GenericActionValue v) {
    process(v.name, true, v.children);
  }

  /**
   * Determine the type of the specified generic node constructor.
   *
   * @param name The constructor's name.
   * @param isAction The flag for whether the constructor appears in an action.
   * @param children The list of bindings for the children.
   */
  protected void process(String name, boolean isAction, List<Binding> children) {
    if (flatten) {
      // Combine [ ..., nt, nt*, ... ] into [ ..., nt*, ... ]
      boolean isCopy = false;
      for (int i=0; i<children.size()-1; i++) {
        // Check whether the first binding is for a nonterminal.
        Binding     b1  = children.get(i);
        if (! (b1.element instanceof NonTerminal)) continue;
        NonTerminal nt1 = (NonTerminal)b1.element;
        
        // Check whether the second binding is for a repeated nonterminal.
        Binding     b2  = children.get(i+1);
        switch (b2.element.tag()) {
        case REPETITION: {
          // Check the preserved repetition.
          Repetition rep = (Repetition)b2.element;
          Binding    bdg = Analyzer.getBinding(((Sequence)rep.element).elements);
          if (null == bdg) {
            throw new IllegalStateException("Malformed repetition " + rep);
          }
          if (! nt1.equals(bdg.element)) continue;
        } break;
          
        case NONTERMINAL: {
          // Check the desugared repetition.
          NonTerminal non  = (NonTerminal)b2.element;
          Production  prod = analyzer.lookup(non);
          if (! nt1.equals(prod.getProperty(Properties.REPEATED))) continue;
        } break;
          
        default:
          continue;
        }
        
        // Success.
        if (! isCopy) {
          children = new ArrayList<Binding>(children);
          isCopy   = true;
        }
        children.remove(i);
        i -= 2; // Check the previous binding again.
        if (-1 > i) i = -1;
      }
    }

    // Determine the types of the bindings.
    final int  size  = isAction ? children.size() + 1 : children.size();
    List<Type> types = new ArrayList<Type>(size);

    if (isAction) {
      if (runtime.test("optionVariant")) {
        // Strip away any list and action types.
        Type t = analyzer.current().type;
        if (AST.isList(t)) t = AST.getArgument(t);
        if (AST.isAction(t)) t = AST.getArgument(t);
        types.add(t);

      } else {
        types.add(AST.NODE);
      }
    }

    for (Binding b : children) {
      types.add(analyzer.type(b.element));
    }

    // Unify the tuple type with any previous tuple type and update
    // its variant.
    final TupleT  t1    = ast.toTuple(name);
    final TupleT  t2    = new TupleT(name, types);
    final boolean fresh = (null == t1.getTypes());

    if (fresh && ! runtime.test("optionVariant")) {
      ast.add(t1, ast.toVariant("Node", false));
    }

    if (DEBUG) {
      runtime.console().p(name).p(" : ");
      ast.print(t2, runtime.console(), false, true, null);
      runtime.console().pln();
    }

    if (! fresh) {
      Type result = ast.combine(t1, t2, flatten, strict);
      if (result.isError()) {
        runtime.error("unable to consistently type tuple '" + name + "'",
                      sequence);
        runtime.errConsole().loc(sequence).p(": error: 1st type is '");
        ast.print(t1, runtime.errConsole(), false, true, null);
        runtime.errConsole().pln("'");
        runtime.errConsole().loc(sequence).p(": error: 2nd type is '");
        ast.print(t2, runtime.errConsole(), false, true, null);
        runtime.errConsole().pln("'").flush();

      } else {
        if (DEBUG) {
          runtime.console().p("  -> ");
          ast.print(result, runtime.console(), false, true, null);
          runtime.console().pln();
        }
        t1.setTypes(result.toTuple().getTypes());
      }

    } else if (flatten) {
      Type result = ast.flatten(t2, strict);
      if (result.isError()) {
        runtime.error("unable to flatten tuple '" + name + "'", sequence);
        runtime.errConsole().loc(sequence).p(": error: type is '");
        ast.print(t2, runtime.errConsole(), false, true, null);
        runtime.errConsole().pln("'").flush();

      } else {
        t1.setTypes(result.toTuple().getTypes());
      }

    } else {
      t1.setTypes(types);
    }
  }

}
