// $Id: daml2pdb.java,v 1.10 2001/02/21 22:15:34 mdean Exp $


/**
 * convert the DAML statements from a file ino a .pdb file
 * suitable for transferring to a Palm handheld.
 *
 * Each .pdb record consists of
 * <ul>
 * <li>type:  char 'R' (Resource), 'L' (Literal), 'S' (Statement), 'H' (Header)
 * <li>value: null-terminated string, URI or literal
 * <li>nstatements:  u24
 * <li>sequence of
 * <ul>
 * <li>type:  char '<' or '>' 
 * <li>predicate:  u24 DB index 
 * <li>value:  u24 DB index
 * </ul>
 * </ul>
 */
class daml2pdb
{
    static int offset = 0;
    
    static java.io.FileOutputStream pdb;

    static int longestRecord = 0;

    static String suffix(String string,
			 char separator)
    {
	int index = string.lastIndexOf(separator);
	if (index == (-1))
	    return string;
	else return string.substring(index + 1);
    }

    static class Node
	implements Comparable
    {
	org.w3c.rdf.model.RDFNode node;
	char type;
	String value;
	String shortValue;
	int index; 

	/**
	 * Statements referencing this node as a subject.
	 */
	java.util.Vector subjects = new java.util.Vector();
	/**
	 * Statements referencing this node as a property.
	 */
	java.util.Vector properties = new java.util.Vector();
	/**
	 * Statements referencing this node as a object.
	 */
	java.util.Vector objects = new java.util.Vector();

	Node(org.w3c.rdf.model.RDFNode node)
	    throws Exception
	{
	    this.node = node;
	    this.type = (node instanceof org.w3c.rdf.model.Literal) ? 'L' : 'R';
	    this.value = node.getLabel();
	    this.shortValue = suffix(this.value, '/');
	}

	protected Node()
	{
	}

	/**
	 * return a sort key based on type.
	 */
	int typeSortKey()
	{
	    switch (type)
		{
		case 'H':
		    return -1;
		case 'R':
		    return 0;
		case 'L':
		    return 1;
		case 'S':
		    return 2;
		default:
		    return 3;
		}
	}

	public int compareTo(Object object)
	{
	    Node node = (Node) object;

	    // sort by type
	    int tsk = typeSortKey();
	    int ntsk = node.typeSortKey();
	    if (tsk != ntsk)
		return (tsk - ntsk);
	    
	    // then by shortValue
	    return shortValue.compareTo(node.shortValue);
	}

	/**
	 * XXX - temporarily avoid record size limit
	 */
	int limitRecordSize()
	{
	    int nstatements = objects.size() + subjects.size();
	    if (nstatements > ((0x10000 - value.length() - 100) / 7))
		{
		    System.err.println("warning:  suppressing " + nstatements + " links for " + value + " due to record size limitation");
		    subjects.clear();
		    objects.clear();
		    nstatements = 0;
		}
	    return nstatements;
	}

	int size()
	{
	    int nstatements = limitRecordSize(); // XXX
	    return 1 + value.length() + 1 + 3 + (nstatements * (1 + 3 + 3));
	}

	void write()
	    throws Exception
	{
	    // sort
	    java.util.Comparator comparator = new java.util.Comparator()
	    {
		public int compare(Object object1,
				   Object object2)
		{
		    try {
			org.w3c.rdf.model.Statement statement1 = (org.w3c.rdf.model.Statement) object1;
			org.w3c.rdf.model.Statement statement2 = (org.w3c.rdf.model.Statement) object2;
			String predicate1 = suffix(statement1.predicate().getLabel(), '#');
			String predicate2 = suffix(statement2.predicate().getLabel(), '#');
			int predicateCompare = predicate1.compareTo(predicate2);
			if (predicateCompare != 0)
			return predicateCompare;
			
			String value1 = suffix(statement1.object().getLabel(), '/');
			String value2 = suffix(statement2.object().getLabel(), '/');
			return value1.compareTo(value2);
		    } catch (Exception e) {
			System.err.println("exception in Comparator:  " + e);
			return 0;
		    }
		}
		public boolean equals(Object object)
		{
		    return false;
		}
	    };
	    java.util.Collections.sort(objects, comparator);
	    java.util.Collections.sort(subjects, comparator);

	    writeChar(type);
	    writeSZ(value);
	    int nstatements = limitRecordSize(); // XXX
	    writeUInt24(nstatements);
	    java.util.Iterator iterator = subjects.iterator();
	    while (iterator.hasNext())
		{
		    org.w3c.rdf.model.Statement statement = (org.w3c.rdf.model.Statement) iterator.next();
		    writeChar('>');
		    writeUInt24(((Node) nodes.get(statement.predicate())).index);
		    writeUInt24(((Node) nodes.get(statement.object())).index);
		}
	    iterator = objects.iterator();
	    while (iterator.hasNext())
		{
		    org.w3c.rdf.model.Statement statement = (org.w3c.rdf.model.Statement) iterator.next();
		    writeChar('<');
		    writeUInt24(((Node) nodes.get(statement.predicate())).index);
		    writeUInt24(((Node) nodes.get(statement.subject())).index);
		}
	}
    }

    /**
     * first node (if present) is a header describing the database.
     */
    static class Header
	extends Node
    {
	int version = 1;
	String description = "";
	boolean firstDB = true;
	String nextDB = "";
	int nliterals = 0;
	int nresources = 0;
	boolean hasStartNode = false;
	int startNode = 0;
	String generator = "$Id: daml2pdb.java,v 1.10 2001/02/21 22:15:34 mdean Exp $";

	Header()
	{
	}

	int size()
	{
	    return 1 + 2 + (description.length() + 1) + 1 + ("".length() + 1) + 3 + 3 + 1 + 3 + (generator.length() + 1);
	}

	void write()
	    throws Exception
	{
	    writeChar('H');
	    writeUInt16(version);
	    writeSZ(description);
	    writeBoolean(firstDB);
	    writeSZ(nextDB);
	    writeUInt24(nliterals);
	    writeUInt24(nresources);
	    writeBoolean(hasStartNode);
	    writeUInt24(startNode);
	    writeSZ(generator);
	}
    }

    /**
     * map RDFNode to Node.
     */
    static java.util.Hashtable nodes = new java.util.Hashtable();
    static Node getNode(org.w3c.rdf.model.RDFNode node)
	throws Exception
    {
	Node retval = (Node) nodes.get(node);

	if (retval == null)
	    {
		retval = new Node(node);
		nodes.put(node, retval);
	    }

	return retval;
    }

    static void writeUInt16(int value)
	throws Exception
    {
	int hi = value / 0x100;
	int low = value % 0x100;
	pdb.write(hi);
	pdb.write(low);
    }

    static void writeUInt32(int value)
	throws Exception
    {
	int u4 = value % 0x100;
	value >>= 8;
	int u3 = value % 0x100;
	value >>= 8;
	int u2 = value % 0x100;
	value >>= 8;
	int u1 = value;

	pdb.write(u1);
	pdb.write(u2);
	pdb.write(u3);
	pdb.write(u4);
    }

    static void writeLocalID(int value)
	throws Exception
    {
	writeUInt32(value);
    }

    static void writeUInt24(int value)
	throws Exception
    {
	int u3 = value % 0x100;
	value >>= 8;
	int u2 = value % 0x100;
	value >>= 8;
	int u1 = value;

	pdb.write(u1);
	pdb.write(u2);
	pdb.write(u3);
    }

    static void writeUniqueID(int value)
	throws Exception
    {
	writeUInt24(value);
    }

    /**
     * write null-terminated String
     */
    static void writeSZ(String string)
	throws Exception
    {
	pdb.write(string.getBytes());
	pdb.write(0);
    }

    static void writeChar(char ch)
	throws Exception
    {
	pdb.write((int) ch);
    }

    static void writeBoolean(boolean value)
	throws Exception
    {
	pdb.write(value ? 'Y' : 'N');
    }

    static int maxStatements = 0xffff;

    static int writeRecordList(java.util.Vector nodes,
			       int start)
	throws Exception
    {
	int nstatements = nodes.size() - start;
	if (nstatements > maxStatements)
	    nstatements = maxStatements;

	// nextRecordListID
	if ((start + nstatements) < nodes.size())
	    {
		int next = offset + 6 + (nstatements * 8);

		for (int i = 0; i < nstatements; i++)
		    {
			next += ((Node) nodes.elementAt(start + i)).size();
		    }
		System.out.println("nextRecordListID = " + next); // XXX
		writeUInt32(next);
	    }
	else
	    writeUInt32(0);

	// numRecords
	writeUInt16(nstatements);

	// placeholder
	if (nstatements == 0)
	    writeUInt16(0);

	offset += 6;
	if (nstatements == 0)
	    offset += 2;

	offset += (nstatements * 8);

	return nstatements;
    }

    static int uniqueID = 1;

    static void writeRecordEntry(int uniqueID)
	throws Exception
    {
	// localChunkID
	writeLocalID(offset);

	// attributes
	pdb.write(0);

	// uniqueID
	writeUniqueID(uniqueID++);
    }

    static void writeDatabaseHeader(String outputfile)
	throws Exception
    {
	// name
	if (outputfile.length() >= 32)
	    {
		System.err.println(outputfile + " must be < 32 characters");
		System.exit(1);
	    }
	pdb.write(outputfile.getBytes());
	for (int i = outputfile.length(); i < 32; i++)
	    pdb.write(0);
	
	// attributes
	writeUInt16(0);

	// version
	writeUInt16(1);
	
	// creationDate
	int now = (int) (System.currentTimeMillis() / 1000L) + 2082844800;
	writeUInt32(now);

	// modificationDate
	writeUInt32(now);

	// lastBackupDate
	writeUInt32(now);

	// modificationNumber
	writeUInt32(0);

	// appInfoID
	writeLocalID(0);

	// sortInfoID
	writeLocalID(0);

	// type
	pdb.write("DAML".getBytes());// XXX

	// creator
	pdb.write("DAML".getBytes());

	// uniqueIDSeed
	writeUInt32(now);	// ?
    }

    static void usage()
    {
	System.err.println("Usage: java -Dorg.xml.sax.parser=<classname> daml2pdb [-[no]namespaces] [-start URI] [-description string] outputfilename <URI | inputfile> ...");
	System.exit(1);
    }

    public static void main(String args[])
    throws Exception
    {
	Header header = new Header();
	org.w3c.rdf.util.RDFFactory f = new org.w3c.rdf.util.RDFFactoryImpl();
	org.w3c.rdf.model.Model m = f.createModel();

	// parse arguments
	String outputfilename = null;
	boolean namespaces = true;
	java.net.URL startURI = null;
	for (int i = 0; i < args.length; i++)
	    {
		String arg = args[i];

		if (arg.charAt(0) == '-')
		    {
			if (arg.equals("-nonamespaces"))
			    namespaces = false;
			else if (arg.equals("-namespaces"))
			    namespaces = true;
			else if (arg.equals("-start"))
			    startURI = new java.net.URL(args[++i]);
			else if (arg.equals("-description"))
			    header.description = args[++i];
			else
			    usage();
		    }
		else if (outputfilename == null)
		    outputfilename = arg;
		else
		    {
			// process input file
			org.w3c.rdf.util.RDFUtil.parse(arg, f.createParser(), m);
		    }
	    }
	if (outputfilename == null)
	    usage();

	// open output file (currently only 1)
	String outputfile = outputfilename + ".pdb";
	pdb = new java.io.FileOutputStream(outputfile);

	// process statements
	java.util.Enumeration en;
	for(en = m.elements(); en.hasMoreElements();)
	    {
		org.w3c.rdf.model.Statement statement = (org.w3c.rdf.model.Statement) en.nextElement();

		Node subject = getNode(statement.subject());
		subject.subjects.add(statement);

		Node property = getNode(statement.predicate());
		property.properties.add(statement);

		Node object = getNode(statement.object());
		object.objects.add(statement);
	    }

	// sort nodes
	java.util.Vector sortedNodes = new java.util.Vector(nodes.values());
	java.util.Collections.sort(sortedNodes);
	sortedNodes.add(0, header);

	// assign indices and count stuff
	int index = 0;
	java.util.Iterator iterator = sortedNodes.iterator();
	while (iterator.hasNext())
	    {
		Node node = (Node) iterator.next();

		node.index = index++;
		switch (node.type)
		    {
		    case 'L':
			header.nliterals++;
			break;
		    case 'R':
			header.nresources++;
			break;
		    default:
		    }
	    }

	// initial node to display
	if (startURI != null)
	    {
		Node startNode = (Node) nodes.get(m.getNodeFactory().createResource(startURI.toString()));
		if (startNode == null)
		    {
			System.err.println("couldn't find " + startURI + " for -start");
			System.exit(1);
		    }
		else
		    {
			header.hasStartNode = true;
			header.startNode = startNode.index;
		    }
	    }

	writeDatabaseHeader(outputfile);
	offset = 0x48;

	int start = 0;
	while (start < sortedNodes.size())
	    {
		int nstatements = writeRecordList(sortedNodes, start);

		for (int i = 0; i < nstatements; i++)
		    {
			Node node = (Node) sortedNodes.elementAt(start + i);
			writeRecordEntry(offset);
			int size = node.size();
			offset += size;
			if (size > longestRecord)
			    longestRecord = size;
		    }

		for (int i = 0; i < nstatements; i++)
		    {
			Node node = (Node) sortedNodes.elementAt(start + i);
			node.write();
		    }
	
		start += nstatements;
	    }

	// print statistics
	System.out.println(m.size() + " statements, " + nodes.size() + " nodes/records, " + offset + " bytes total, " + longestRecord + " bytes in longest record");
    }
}
