/**
 * Copyright (c) 2004-2006 Regents of the University of California.
 * See "license-prefuse.txt" for licensing terms.
 */
package prefuse.data.io;

import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Date;
import java.util.HashMap;

import prefuse.data.Graph;
import prefuse.data.Node;
import prefuse.data.Schema;
import prefuse.util.io.XMLWriter;

/**
 * GraphWriter instance that writes a tree file formatted using the
 * TreeML file format. TreeML is an XML format originally created for
 * the 2003 InfoVis conference contest. A DTD (Document Type Definition) for
 * TreeML is
 * <a href="http://www.nomencurator.org/InfoVis2003/download/treeml.dtd">
 *  available online</a>.
 * 
 * <p>The GraphML spec only supports the data types <code>Int</code>,
 * <code>Long</code>, <code>Float</code>, <code>Real</code> (double),
 * <code>Boolean</code>, <code>String</code>, and <code>Date</code>.
 * An exception will be thrown if a data type outside these allowed
 * types is encountered.</p>
 * 
 * @author <a href="http://jheer.org">jeffrey heer</a>
 */
public class TreeMLWriter extends AbstractGraphWriter {

    /**
     * String tokens used in the TreeML format.
     */
    public interface Tokens extends TreeMLReader.Tokens {}
    
    /**
     * Map containing legal data types and their names in the GraphML spec
     */
    private static final HashMap TYPES = new HashMap();
    static {
        TYPES.put(int.class, Tokens.INT);
        TYPES.put(long.class, Tokens.LONG);
        TYPES.put(float.class, Tokens.FLOAT);
        TYPES.put(double.class, Tokens.REAL);
        TYPES.put(boolean.class, Tokens.BOOLEAN);
        TYPES.put(String.class, Tokens.STRING);
        TYPES.put(Date.class, Tokens.DATE);
    }
    
    /**
     * @see prefuse.data.io.GraphWriter#writeGraph(prefuse.data.Graph, java.io.OutputStream)
     */
    public void writeGraph(Graph graph, OutputStream os) throws DataIOException
    {
        // first, check the schemas to ensure GraphML compatibility
        Schema ns = graph.getNodeTable().getSchema();
        checkTreeMLSchema(ns);
        
        XMLWriter xml = new XMLWriter(new PrintWriter(os));
        xml.begin();
        
        xml.comment("prefuse TreeML Writer | "
                + new Date(System.currentTimeMillis()));
                
        // print the tree contents
        xml.start(Tokens.TREE);
        
        // print the tree node schema
        xml.start(Tokens.DECLS);
        String[] attr = new String[] {Tokens.NAME, Tokens.TYPE };
        String[] vals = new String[2];

        for ( int i=0; i<ns.getColumnCount(); ++i ) {
            vals[0] = ns.getColumnName(i);
            vals[1] = (String)TYPES.get(ns.getColumnType(i));
            xml.tag(Tokens.DECL, attr, vals, 2);
        }
        xml.end();
        xml.println();
        
        
        // print the tree nodes
        attr[0] = Tokens.NAME;
        attr[1] = Tokens.VALUE;
        
        Node n = graph.getSpanningTree().getRoot();
        while ( n != null ) {
            boolean leaf = (n.getChildCount() == 0);
            
            if ( leaf ) {
                xml.start(Tokens.LEAF);
            } else {
                xml.start(Tokens.BRANCH);
            }
            
            if ( ns.getColumnCount() > 0 ) {
                for ( int i=0; i<ns.getColumnCount(); ++i ) {
                    vals[0] = ns.getColumnName(i);
                    vals[1] = n.getString(vals[0]);
                    xml.tag(Tokens.ATTR, attr, vals, 2);
                }
            }
            n = nextNode(n, xml);
        }
        
        // finish writing file
        xml.end();
        xml.finish();
    }
    
    /**
     * Find the next node in the depth first iteration, closing off open
     * branch tags as needed.
     * @param x the current node
     * @param xml the XMLWriter
     * @return the next node
     */
    private Node nextNode(Node x, XMLWriter xml) {
        Node n, c;
        if ( (c=x.getChild(0)) != null ) {
            // do nothing
        } else if ( (c=x.getNextSibling()) != null ) {
            xml.end();
        } else {
            c = x.getParent();
            xml.end();
            while ( c != null ) {
                if ( (n=c.getNextSibling()) != null ) {
                    c = n;
                    xml.end();
                    break;
                }
                c = c.getParent();
                xml.end();
            }
        }
        return c;
    }
    
    /**
     * Checks if all Schema types are compatible with the TreeML specification.
     * The TreeML spec only allows the types <code>int</code>,
     * <code>long</code>, <code>float</code>, <code>double</code>,
     * <code>boolean</code>, <code>string</code>, and <code>date</code>.
     * @param s the Schema to check
     */
    private void checkTreeMLSchema(Schema s) throws DataIOException {
        for ( int i=0; i<s.getColumnCount(); ++i ) {
            Class type = s.getColumnType(i);
            if ( TYPES.get(type) == null ) {
                throw new DataIOException("Data type unsupported by the "
                    + "TreeML format: " + type.getName());
            }
        }
    }
    
} // end of class GraphMLWriter