/*
 * xtc - The eXTensible Compiler
 * Copyright (C) 2007-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.type;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import xtc.Constants;

import xtc.tree.Attribute;
import xtc.tree.Printer;
import xtc.tree.VisitingException;
import xtc.tree.Visitor;

/**
 * A visitor to print types in C-like source form.  This visitor's
 * functionality is available through the {@link #print(Type)} and
 * {@link #print(Type,String)} methods.
 *
 * @author Robert Grimm
 * @version $Revision: 1.6 $
 */
public class SourcePrinter extends Visitor {

  /** The set of attributes to print literally. */
  private static Set<Attribute> LITERALS = new HashSet<Attribute>();

  static {
    LITERALS.add(Constants.ATT_ABSTRACT);
    LITERALS.add(Constants.ATT_CONSTANT); // Must be printed separately.
    LITERALS.add(Constants.ATT_INLINE);
    LITERALS.add(Constants.ATT_NATIVE);
    LITERALS.add(Constants.ATT_RESTRICT);
    LITERALS.add(Constants.ATT_STRICT_FP);
    LITERALS.add(Constants.ATT_SYNCHRONIZED);
    LITERALS.add(Constants.ATT_THREAD_LOCAL); // Must be printed separately.
    LITERALS.add(Constants.ATT_TRANSIENT);
    LITERALS.add(Constants.ATT_VOLATILE);
  }

  /** The printer utility. */
  protected final Printer printer;

  /** The base type. */
  protected Type base;

  /** The list of derived types, with the outer-most type first. */
  protected List<Type> derived;

  /** The optional variable name. */
  protected String name;

  /**
   * The flag for whether the next token needs to be preceded by a
   * space.
   */
  protected boolean needsSpace;

  /**
   * Create a new source printer.  Note that this visitor is
   * <em>not</em> registered with the printer utility.
   *
   * @param printer The printer utility.
   */
  public SourcePrinter(Printer printer) {
    this.printer = printer;
  }

  /**
   * Print the specified type.  If the specified type contains a
   * {@link VariableT}, this method prints a declaration for that
   * type's name.  Otherwise, it prints an abstract declaration.  Note
   * that the printed declaration is <em>not</em> followed by any
   * delimiter such as a semicolon.
   *
   * @param type The type.
   * @throws IllegalArgumentException Signals that the specified type
   *   contains an error type or more than one field, member,
   *   parameter, or variable type.
   */
  public void print(Type type) {
    print(type, null);
  }

  /**
   * Print the specified type and variable as a declaration.  Note
   * that the printed declaration is <em>not</em> followed by any
   * delimiter such as a semicolon.
   *
   * @param type The type.
   * @param variable The variable name.
   * @throws IllegalArgumentException Signals that the specified type
   *   contains a field, member, parameter, variable, or error type.
   */
  public void print(Type type, String variable) {
    // Save internal state.
    Type       savedBase       = base;
    List<Type> savedDerived    = derived;
    String     savedName       = name;
    boolean    savedNeedsSpace = needsSpace;

    base       = type;
    derived    = null;
    name       = variable;
    needsSpace = false;

    try {
      dispatch(type);
    } catch (VisitingException x) {
      // Unwrap runtime exceptions.
      if (x.getCause() instanceof RuntimeException) {
        throw (RuntimeException)x.getCause();
      } else {
        throw x;
      }
    } finally {
      // Restore internal state.
      base       = savedBase;
      derived    = savedDerived;
      name       = savedName;
      needsSpace = savedNeedsSpace;
    }
  }

  /**
   * Set the variable name.
   *
   * @param variable The variable name.
   * @throws IllegalArgumentException Signals a duplicate variable
   *   name.
   */
  protected void setVariable(String variable) {
    if (null != name) {
      throw new IllegalArgumentException("duplicate variable name");
    } else {
      name = variable;
    }
  }

  /**
   * Add the specified type to the list of derived types.
   *
   * @param type The type.
   */
  protected void addDerived(Type type) {
    if (null == derived) derived = new ArrayList<Type>();
    derived.add(type);
  }

  /**
   * Emit a space.  If the {@link #needsSpace} flag is set, this
   * method emits the space and clears the flag.
   */
  protected void space() {
    if (needsSpace) {
      printer.p(' ');
      needsSpace = false;
    }
  }

  /**
   * Emit any derived types.  This method also prints the variable
   * name, if it is available.  It must be invoked after printing a
   * non-derived type.
   */
  protected void printDerived() {
    /*
    if (null != derived) {
      for (int i=0; i<derived.size(); i++) {
        System.out.print(i);
        System.out.print(": ");
        System.out.println(derived.get(i).toString());
      }
    }
    */

    // Print any pointer types.
    if (null != derived) {
      boolean isPointer = true; // Track nested array or function types.
      for (int i=derived.size()-1; i>=0; i--) {
        Type t = derived.get(i);
        Type r = t.resolve();

        if (! r.isPointer()) {
          // Remember a nested array or function type.
          isPointer = false;
        } else {
          // Print a parenthesis for a nested array or function type.
          if (! isPointer) {
            space();
            printer.p('(');
            needsSpace = false;
            isPointer  = true;
          }

          // Print the pointer.
          printPointer(t);
        }
      }
    }

    // Print the variable name if available.
    if (null != name) {
      space();
      printer.p(name);
    }

    // Print any array and function types.
    if (null != derived) {
      final int size      = derived.size();
      boolean   isPointer = false;
      for (int i=0; i<size; i++) {
        Type t = derived.get(i);
        Type r = t.resolve();

        if (r.isPointer()) {
          // Print a parenthesis for a nested array or function type.
          if (! isPointer) {
            for (int j=i+1; j<size; j++) {
              if (! derived.get(j).resolve().isPointer()) {
                space();
                printer.p(')');
                break;
              }
            }
            isPointer = true;
          }

        } else if (r.isArray()) {
          isPointer = false;
          printArray(t);

        } else if (r.isFunction()) {
          isPointer = false;
          printFunction(t);
        }
      }
    }
  }

  /**
   * Print the specified pointer type.
   *
   * @param type The pointer type, which may be wrapped
   */
  protected void printPointer(Type type) {
    // Verify the pointer type.
    if (! type.resolve().isPointer()) {
      throw new IllegalStateException("printing non-pointer as pointer");
    }

    // Print the pointer.
    space();
    printer.p('*');

    // Print any qualifiers.
    if (hasAttributes(type)) {
      printer.p(' ');
      printAttributes(type);
    }
  }

  /**
   * Print the specified array type.
   *
   * @param type The array type, which may be wrapped.
   */
  protected void printArray(Type type) {
    // Resolve and verify the array type.
    Type r = type.resolve();
    if (! r.isArray()) {
      throw new IllegalStateException("printing non-array as array");
    }

    // Print the array type.
    ArrayT a = r.toArray();
    space();
    printer.p('[');

    // Print any qualifiers.
    if (hasAttributes(type)) {
      printAttributes(type);
    }

    // Print any length.
    if (a.isVarLength()) {
      space();
      printer.p('*');
    } else if (a.hasLength()) {
      space();
      printer.p(a.getLength());
    }

    printer.p(']');
  }

  /**
   * Print the specified function type.
   *
   * @param type The function type, which may be wrapped.
   */
  protected void printFunction(Type type) {
    // Resolve and verify the function type.
    Type r = type.resolve();
    if (! r.isFunction()) {
      throw new IllegalStateException("printing non-function as function");
    }

    // Print the function type.
    FunctionT f = (FunctionT)r;
    space();
    printer.p('(');

    if (f.hasAttribute(Constants.ATT_STYLE_NEW)) {
      List<Type> params = f.getParameters();
      if (0 == params.size()) {
        printer.p("void");
      } else {
        for (Iterator<Type> iter=params.iterator(); iter.hasNext(); ) {
          print(iter.next());
          if (iter.hasNext()) printer.p(", ");
        }
      }
    }

    printer.p(')');
  }

  /**
   * Determine whether the specified attribute is printable.
   *
   * @param att The attribute.
   * @return <code>true</code> if the attribute is printable.
   */
  public static boolean isPrintable(Attribute att) {
    if (LITERALS.contains(att)) return true;

    String name = att.getName();
    return (Constants.NAME_STORAGE.equals(name) ||
            (Constants.NAME_VISIBILITY.equals(name) &&
             (! Constants.ATT_PACKAGE_PRIVATE.equals(att))));
  }

  /**
   * Determine whether the specified type or any wrapped types have
   * any printable attributes.
   *
   * @param type The type.
   * @return <code>true</code> if the type or any wrapped types have
   *   any printable attributes.
   */
  public static boolean hasAttributes(Type type) {
    do {
      if (type.hasAttributes()) {
        for (Attribute att : type.attributes()) {
          if (isPrintable(att)) return true;
        }
      }

      if (! type.isWrapped()) return false;
      type = type.toWrapped().getType();
    } while (true);
  }

  /**
   * Print the attributes for the specified type and any wrapped
   * types.
   *
   * @param type The type.
   */
  protected void printAttributes(Type type) {
    do {
      if (type.hasAttributes()) {
        for (Attribute att : type.attributes()) dispatch(att);
      }

      if (! type.isWrapped()) return;
      type = type.toWrapped().getType();
    } while (true);
  }

  /** Print the specified attribute. */
  public void visit(Attribute att) {
    String name = att.getName();

    if (Constants.NAME_STORAGE.equals(name)) {
      space();
      printer.p(att.getValue().toString());
      needsSpace = true;

    } else if (Constants.NAME_VISIBILITY.equals(name)) {
      if (! Constants.ATT_PACKAGE_PRIVATE.equals(att)) {
        space();
        printer.p(att.getValue().toString());
        needsSpace = true;
      }

    } else if (Constants.ATT_CONSTANT.equals(att)) {
      space();
      printer.p("const");
      needsSpace = true;

    } else if (Constants.ATT_THREAD_LOCAL.equals(att)) {
      space();
      printer.p("__thread");
      needsSpace = true;

    } else if (LITERALS.contains(att)) {
      space();
      printer.p(name);
      needsSpace = true;
    }
  }

  /** Print the specified void type. */
  public void visit(VoidT t) {
    printAttributes(base);
    space();
    printer.p("void");
    needsSpace = true;
    printDerived();
  }

  /** Print the specified number type. */
  public void visit(NumberT t) {
    printAttributes(base);
    space();
    printer.p(t.toString());
    needsSpace = true;
    printDerived();
  }

  /** Print the specified struct type. */
  public void visit(StructT t) {
    if (t.isUnnamed()) throw new IllegalArgumentException("anonymous struct");
    printAttributes(base);
    space();
    printer.p("struct ").p(t.getName());
    needsSpace = true;
    printDerived();
  }

  /** Print the specified union type. */
  public void visit(UnionT t) {
    if (t.isUnnamed()) throw new IllegalArgumentException("anonymous union");
    printAttributes(base);
    space();
    printer.p("union ").p(t.getName());
    needsSpace = true;
    printDerived();
  }

  /** Print the specified enum type. */
  public void visit(EnumT t) {
    if (t.isUnnamed()) throw new IllegalArgumentException("anonymous enum");
    printAttributes(base);
    space();
    printer.p("enum " ).p(t.getName());
    needsSpace = true;
    printDerived();
  }

  /** Print the specified class or interface type. */
  public void visit(ClassOrInterfaceT t) {
    space();
    printer.p(t.getName());
    needsSpace = true;
    printDerived();
  }

  /** Print the specified alias type. */
  public void visit(AliasT t) {
    printAttributes(base);
    space();
    printer.p(t.getName());
    needsSpace = true;
    printDerived();
  }

  /** Print the specified internal type. */
  public void visit(InternalT t) {
    printAttributes(base);
    space();
    printer.p(t.getName());
    needsSpace = true;
    printDerived();
  }

  /** Print the specified pointer type. */
  public void visit(PointerT t) {
    addDerived(base);
    base = t.getType();
    dispatch(base);
  }

  /** Print the specified array type. */
  public void visit(ArrayT t) {
    addDerived(base);
    base = t.getType();
    dispatch(base);
  }
  
  /** Print the specified function type. */
  public void visit(FunctionT t) {
    addDerived(base);
    base = t.getResult();
    dispatch(base);
  }

  /** Print the specified variable type. */
  public void visit(VariableT t) {
    setVariable(t.getName());
    dispatch(t.getType());
  }

  /** Print the specified wrapped type. */
  public void visit(WrappedT t) {
    dispatch(t.getType());
  }

  /** Print the specified error type. */
  public void visit(ErrorT t) {
    throw new IllegalArgumentException("error type");
  }

}