/*
 * This file is part of the DistSim distributed simulation framework (hibernate-extension)
 * Copyright (C) 2007 Ulf Hermann; 2003-2006 Doug Currie, doug.currie@alum.mit.edu and others
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
//$Id: MapGenerator.java,v 1.2 2003/11/04 17:39:33 oneovthafew Exp $
package brn.distsim.ormapper.class2hbm;

import java.io.Writer;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;

import org.hibernate.type.AnyType;
import org.hibernate.type.Type;
import org.hibernate.type.TypeFactory;
import org.hibernate.util.StringHelper;
import org.python.core.PyInstance;

/**
 * The following description is somewhat outdated. I used the original hibernate
 * extensions as a starting point to build a fully automatic OR-Mapper which can
 * create reasonable hibernate mappings from as many classes as possible. Also
 * especially important was the requirement that all created mappings should be
 * correct, regardless if the all requested classes were actually mapped and
 * that there should be a clear distinction between mappable and unmappable
 * classes. The original hibernate extensions have the goal of providing a
 * starting point for manually creating a good mapping for known classes.
 * Obviously, I had to change most of the code to fit my needs.
 *
 * Ulf Hermann - 2006/12/20
 *
 * <P>
 * MapGenerator provides a mechanism to produce a Hibernate XML OR-Mapping from
 * compiled classes. It does this using Java reflection to find <b>properties</b>
 * to be persisted in the classes, and using the types of the properties to
 * further guide the reflection.
 * <P>
 * The usual way to use MapGenerator is to place your compiled classes on the
 * classpath, and start Java in the MapGenerator static main() method. As
 * arguments you can either supply all of the classes to be processed, or the
 * single argument <code>--interact</code> which will provide an interactive
 * prompt/response console. Using this mode you can set the UID property name
 * for each class using the <code>uid=XXX</code> command where XXX is the UID
 * property name. Other command alternatives are simply a fully qualified class
 * name, or the command <code>done</code> which emits the XML and terminates.
 *
 * <P>
 * MapGenerator will reject classes that are not <b>hibernate perisitable</b>.
 * To be hibernate persistable a class must not be a primitive type, an array,
 * an interface, or a nested class, and it must have a default (zero argument)
 * constructor.
 *
 * <P>
 * MapGenerator will climb the superclass chain of all added classes attempting
 * to add as many hibernate perisitable superclasses as possible to the same
 * database table. The search stops as soon as a property is found that has a
 * name appearing on the list of candidate UID names, and has type String, Long,
 * or long.
 *
 * <P>
 * Properties are discovered when there are two methods in the class, a setter
 * and a getter, where the type of the setter's single argument is the same as
 * the return type of the zero argument getter, and the setter returns
 * <code>void</code>. Furthermore, the setter's name must start with the
 * string "set" and either the getter's name starts with "get" or the getter's
 * name starts with "is" and the type of the property is <code>boolean</code>.
 * In either case, the remainder of their names must match. This matching
 * portion is the name of the property, except that the initial character of the
 * property name is made lower case if the second letter is lower case.
 *
 * <P>
 * The rules for determing the database type of each property are as follows. If
 * the Java type is Hibernate.basic(), then the property is a simple column of
 * that type. For hibernate.type.Type custom types and PersistentEnum a simple
 * column is used as well. If the property type is an array, then a Hibernate
 * array is used, and MapGenerator attempts to reflect on the array element
 * type. If the property has type java.util.List, java.util.Map, or
 * java.util.Set, then the corresponding Hibernate types are used, but
 * MapGenerator cannot further process the insides of these types. If the
 * property's type is any other class, MapGenerator defers the decision on the
 * database representation until all classes have been processed (i.e., until
 * <code>done</code> is typed at the interactive prompt. At this point, if the
 * class was discovered through the superclass search described above, then the
 * property is an association (many-to-one). If the class has any properties,
 * then it is a component. Otherwise it is serializable, or not persistable.
 *
 *
 * @version 1.x
 * @author <a href="mailto:doug.currie@alum.mit.edu">e</a>
 * @auther <a href="mailto:uhermann@informatik.hu-berlin.de">Ulf Hermann</a>
 */
public class MapGenerator {

	private ClassLoader classLoader;

	/**
	 * the XML we make; this buffer is shared by all string emitters created by
	 * this MapGenerator instance
	 */
	protected StringBuffer buf;

	/**
	 * a cache of seen reflected classes; Class -> ReflectedClass <br>
	 * also necessary to avoid infinite regress
	 */
	protected Hashtable rClasses;

	/**
	 * just to prevent duplicate table names; String -> Integer
	 */
	private Hashtable usedTableNames = new Hashtable();

	private static Set reservedSQLWords;

	/**
	 * the top level (most souper) ReflectedClasses; <br>
	 * these are the classes eventually dumped as <class> in the XML Yet there
	 * is only one root: java.lang.Object
	 */
	ReflectedClass root;

	private char[] prefix = StringHelper.repeat("\t", 100).toCharArray();

	/**
	 * a cache of emitted components for the current property; Class -> Integer
	 * also necessary to avoid infinite regress by tracking the depth of data
	 * recursion
	 */
	protected Set cycleBuster;

	static {
		reservedSQLWords = new HashSet();
		String raw = " ADD 	ALL 	ALTER ANALYZE 	AND 	AS ASC 	ASENSITIVE 	BEFORE BETWEEN 	BIGINT 	BINARY BLOB 	BOTH 	BY CALL 	CASCADE 	CASE CHANGE 	CHAR 	CHARACTER CHECK 	COLLATE 	COLUMN CONDITION 	CONNECTION 	CONSTRAINT CONTINUE 	CONVERT 	CREATE CROSS 	CURRENT_DATE 	CURRENT_TIME CURRENT_TIMESTAMP 	CURRENT_USER 	CURSOR DATABASE 	DATABASES 	DAY_HOUR DAY_MICROSECOND 	DAY_MINUTE 	DAY_SECOND DEC 	DECIMAL 	DECLARE DEFAULT 	DELAYED 	DELETE DESC 	DESCRIBE 	DETERMINISTIC DISTINCT 	DISTINCTROW 	DIV DOUBLE 	DROP 	DUAL EACH 	ELSE 	ELSEIF ENCLOSED 	ESCAPED 	EXISTS EXIT 	EXPLAIN 	FALSE FETCH 	FLOAT 	FLOAT4 FLOAT8 	FOR 	FORCE FOREIGN 	FROM 	FULLTEXT GRANT 	GROUP 	HAVING HIGH_PRIORITY 	HOUR_MICROSECOND 	HOUR_MINUTE HOUR_SECOND 	IF 	IGNORE IN 	INDEX 	INFILE INNER 	INOUT 	INSENSITIVE INSERT 	INT 	INT1 INT2 	INT3 	INT4 INT8 	INTEGER 	INTERVAL INTO 	IS 	ITERATE JOIN 	KEY 	KEYS KILL 	LEADING 	LEAVE LEFT 	LIKE 	LIMIT LINES 	LOAD 	LOCALTIME LOCALTIMESTAMP 	LOCK 	LONG LONGBLOB 	LONGTEXT 	LOOP LOW_PRIORITY 	MATCH 	MEDIUMBLOB MEDIUMINT 	MEDIUMTEXT 	MIDDLEINT MINUTE_MICROSECOND 	MINUTE_SECOND 	MOD MODIFIES 	NATURAL 	NOT NO_WRITE_TO_BINLOG 	NULL 	NUMERIC ON 	OPTIMIZE 	OPTION OPTIONALLY 	OR 	ORDER OUT 	OUTER 	OUTFILE PRECISION 	PRIMARY 	PROCEDURE PURGE 	RAID0 	READ READS 	REAL 	REFERENCES REGEXP 	RELEASE 	RENAME REPEAT 	REPLACE 	REQUIRE RESTRICT 	RETURN 	REVOKE RIGHT 	RLIKE 	SCHEMA SCHEMAS 	SECOND_MICROSECOND 	SELECT SENSITIVE 	SEPARATOR 	SET SHOW 	SMALLINT 	SONAME SPATIAL 	SPECIFIC 	SQL SQLEXCEPTION 	SQLSTATE 	SQLWARNING SQL_BIG_RESULT 	SQL_CALC_FOUND_ROWS 	SQL_SMALL_RESULT SSL 	STARTING 	STRAIGHT_JOIN TABLE 	TERMINATED 	THEN TINYBLOB 	TINYINT 	TINYTEXT TO 	TRAILING 	TRIGGER TRUE 	UNDO 	UNION UNIQUE 	UNLOCK 	UNSIGNED UPDATE 	USAGE 	USE USING 	UTC_DATE 	UTC_TIME UTC_TIMESTAMP 	VALUES 	VARBINARY VARCHAR 	VARCHARACTER 	VARYING WHEN 	WHERE 	WHILE WITH 	WRITE 	X509 XOR 	YEAR_MONTH 	ZEROFILL ASENSITIVE CALL CONDITION CONNECTION CONTINUE CURSOR DECLARE DETERMINISTIC EACH ELSEIF EXIT FETCH GOTO INOUT INSENSITIVE ITERATE LABEL LEAVE LOOP MODIFIES OUT READS RELEASE REPEAT RETURN SCHEMA SCHEMAS SENSITIVE SPECIFIC SQL SQLEXCEPTION SQLSTATE SQLWARNING TRIGGER UNDO UPGRADE WHILE";
		String words[] = raw.split("[ \t]");
		for (int i = 0; i < words.length; ++i) {
			reservedSQLWords.add(words[i]);
		}
	}

	/**
	 * the only MapGenerator constructor
	 *
	 * @param className
	 *            an array of fully specified class names to add, or null
	 */
	public MapGenerator(String[] className, ClassLoader loader) {
		setClassLoader(loader);
		reset();
		if (className != null) {
			int len = className.length;
			for (int i = 0; i < len; i++) {
				addClass(className[i], false);
			}
		}
	}

	public void writeXML(Writer outputWriter) {
		String xml = getXML();
		if (outputWriter != null) {
			try {
				outputWriter.write(xml);
				outputWriter.flush();
			} catch (Exception e) {
				outputWriter = null;
			}
		}
	}

	public boolean isMapped(Class c) {
		boolean ret = rClasses.containsKey(c.getName());
		return ret;
	}

	public Set getMappedClasses() {
		return rClasses.keySet();
	}

	/**
	 * start over
	 */
	public void reset() {
		this.buf = new StringBuffer();
		this.root = null;
		this.rClasses = new Hashtable();
		this.usedTableNames = new Hashtable();
	}

	public void addPyClass(PyInstance pyInstance, String name) {
    ReflectedClass rc = reallyAdd(pyInstance, name);
    if (rc == null) {
      System.err.println("<!-- " + name
          + " cannot be added, no UID found! -->");
    }
  }

  /**
	 * add a class to the map and reflect upon it
	 *
	 * @param className
	 *            a fully qualified class name in the class path
	 * @param verbose
	 *            squawk (as a comment into the final XML) if there are problems
	 *            with this class
	 */
	public void addClass(String className, boolean verbose) {
		Class clazz = checkClassNamed(className, verbose);
		if (clazz != null) {
			ReflectedClass rc = reallyAdd(clazz, verbose);
			if (rc == null && verbose) {
				System.err.println("<!-- " + clazz.getName()
						+ " cannot be added, no UID found! -->");
			}
		}
	}
  private ReflectedClass reallyAdd(PyInstance pyInstance, String name) {
    boolean verbose = true;
    ReflectedClass rc = (ReflectedClass) rClasses.get(name);
    if (rc == null) {
      // not already added
      rc = new ReflectedPyClass(this, pyInstance, name);
    } else if (rc.isPersistent()) {
      if (verbose) {
        System.err.println("<!-- " + name
            + " already added -->");
      }
      return rc;
    }
    rc.setPersistent(true);

    // TODO superclass  
    assert (pyInstance.instclass.__bases__.size() <= 1);
    Class pySuper = (Class) pyInstance.instclass.__bases__.get(0);
//    PyClass pySuper = (PyClass) pyInstance.instclass.__bases__.get(0); 
    assert (pySuper.getName().equals(Object.class.getName()));
    Class superclass = Object.class; 
    // add superclasses 'til root is found
    if (superclass == null) {
      rc.addSuperclassProps();
      root = rc;
      // report!? (addClass will do it when it sees null)
    } else {

      ReflectedClass sup = reallyAdd(superclass, false); // recurse
      sup.addReflectedClass(rc);
    }
    return rc;
  }

  
	private ReflectedClass reallyAdd(Class clazz, boolean verbose) {
		ReflectedClass rc = (ReflectedClass) rClasses.get(clazz.getName());
		Class superclass = clazz.getSuperclass();
		if (rc == null) {
			// not already added
			rc = new ReflectedClass(this, clazz);
		} else if (rc.isPersistent()) {
			if (verbose) {
				System.err.println("<!-- " + clazz.getName()
						+ " already added -->");
			}
			return rc;
		}
		rc.setPersistent(true);

		// add superclasses 'til root is found
		if (superclass == null) {
			rc.addSuperclassProps();
			root = rc;
			// report!? (addClass will do it when it sees null)
		} else {

			ReflectedClass sup = reallyAdd(superclass, false); // recurse
			sup.addReflectedClass(rc);
		}
		return rc;
	}

	/**
	 * used to make unique table and column names
	 *
	 * @param best
	 *            is the desired name
	 * @param h
	 *            is the uniqifying hashtable
	 * @return the unique name
	 */
	protected String nextName(String best, Hashtable h) {
		Integer seen = (Integer) h.get(best);
		if (seen == null) {
			h.put(best, new Integer(1));
			if (!reservedSQLWords.contains(best.toUpperCase())) {
				// all set
				return best;
			} else {
				return (best + "_" + 0);
			}
		}
		h.put(best, new Integer(seen.intValue() + 1));
		return (best + "_" + seen);
	}

	protected String tableNameFor(String name) {
		String best = name.replaceAll("\\.", "_");
		// TO DO check for illegal names?
		// check for duplicate names...
		return nextName(best, usedTableNames);
	}

	protected String columnNameFor(String name) {
		String best = name.replaceAll("\\.", "_");
		if (reservedSQLWords.contains(best.toUpperCase()))
			best = best + "_" + 0;
		return best;
		// TO DO check for illegal names?
		// check for duplicate names...
	}

	/**
	 * adds spaces to the front of lines in buf for indentation
	 *
	 * @param n
	 *            indentation level
	 */
	protected void emitPrefix(int n) {
		int e = n * 1; // configurable?
		if (e > prefix.length)
			e = prefix.length;
		buf.append(prefix, 0, e);
	}

	/**
	 * after all classes are added
	 *
	 * @return the XML
	 */
	public String getXML() {
		buf
				.append("<?xml version=\"1.0\"?>\n")
				.append("<!DOCTYPE hibernate-mapping PUBLIC\n")
				.append("\t\"-//Hibernate/Hibernate Mapping DTD 3.0//EN\"\n")
				.append(
						"\t\"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd\">\n")
				.append("<hibernate-mapping auto-import='false'>\n");

		// xml the classes
		root.getXML(1);

		buf.append("</hibernate-mapping>\n");
		String ret = buf.toString();
		buf = new StringBuffer(buf.length());
		usedTableNames.clear();
		return ret;
	}

	private Class checkClassNamed(String className, boolean v) {
		try {
			Class clazz = classLoader.loadClass(className);
			return checkClass(clazz, className, v);
		} catch (Exception e) {
			if (v)
				System.err.println("<!-- Class " + className
						+ " gave exception " + e + " -->");
			return null;
		}
	}

	/**
	 * this is the factory to make a ReflectedProperty <br>
	 * using this factory will insure that the property types are inferred
	 * consistently
	 *
	 * @param name
	 *            the property name
	 * @param cls
	 *            the property class
	 * @return the new ReflectedProperty
	 */
	protected ReflectedProperty makeProperty(String className, String name,
			Class cls) {
		String tynm = cls.getName();
		Type htyp = TypeFactory.basic(tynm);

		if (htyp != null && !(htyp instanceof AnyType)) {
			return new ReflectedProperty(name, cls, this, "basic", className);
		} else if (cls.isArray()) {
			return new ReflectedArrayProperty(name, cls, this, className);
		}

		// differs from Hibernate.auto...
		else if (java.util.List.class.equals(cls)) {
			return new ReflectedListProperty(name, cls, this, className);
		} else if (java.util.Map.class.equals(cls)) {
			return new ReflectedMapProperty(name, cls, this, className);
		} else if (java.util.Set.class.equals(cls)) {
			return new ReflectedSetProperty(name, cls, this, className);
		} else if (java.util.Collection.class.equals(cls)) {
			return new ReflectedCollectionProperty(name, cls, this, className);
		}
		// leave the hard stuff for ReflectedComponent...
		else {
			Class comp = checkComponent(cls, tynm, true);
			if (comp == null) {
				return new ReflectedProperty(name, cls, this, "custom",
						className);
			}
			ReflectedClass rc = (ReflectedClass) rClasses.get(comp.getName());
			if (rc == null) {
				// not already processed (avoid infinite regress!)
				if (checkClass(comp, comp.getName(), true) != null)
					rc = reallyAdd(comp, false);
				else
					rc = new ReflectedClass(this, comp);
			}
			return new ReflectedComponent(name, comp, this, rc, className);
		}
	}

	/**
	 * verify that a class is <b>hibernate-persistable</b>
	 *
	 * @param clazz
	 *            the class to ckeck
	 * @param className
	 *            the name of the class for error reporting
	 * @param v
	 *            verbose - should be true for supplied classes, but false for
	 *            chased superclasses
	 * @return the class clazz if it is hibernate-persistable, else null
	 */
	protected Class checkClass(Class clazz, String className, boolean v) {
		if (clazz.isInterface()) {
			if (v)
				System.err.println("<!-- Class " + className
						+ " is an interface! -->");
			return null;
		}
		return checkClCoGuts(clazz, className, v, "<!-- Class ");
	}

	/**
	 * verify that a class is <b>hibernate-persistable</b> as a component
	 *
	 * @param clazz
	 *            the class to ckeck
	 * @param className
	 *            the name of the class for error reporting
	 * @param v
	 *            verbose - should probably always be true, but can be false to
	 *            ignore the property's status
	 * @return the class clazz if it is hibernate-persistable as a component,
	 *         else null
	 */
	protected Class checkComponent(Class clazz, String className, boolean v) {
		if (clazz.isInterface()) {
			return Object.class;
		}
		return checkClCoGuts(clazz, className, v, "<!-- Component ");
	}

	private Class checkClCoGuts(Class clazz, String className, boolean v,
			String c) {
		if (clazz.isPrimitive()) {
			if (v)
				System.err.println(c + className + " is a primitive! -->");
			return null;
		}
		if (clazz.isArray()) {
			if (v)
				System.err.println(c + className + " is an array! -->");
			return null;
		}
		// the class is OK to perisist
		return clazz;
	}

	public void setClassLoader(ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	public ReflectedClass getReflectedClass(Class c) {
		return (ReflectedClass) rClasses.get(c.getName());
	}

  public ReflectedClass getReflectedClass(String name) {
    return (ReflectedClass) rClasses.get(name);
  }

}

/*
 * TO DO new dtd accommodated? length property for string/blob/binary? top level
 * collections (see PEntity and entities) (nice)... maybe also give access to
 * ReflectedClass so callers can set... UID property name and generator (or
 * heuristically?)... get real UID, index, and data colummns for collections
 * nesting into collections optimistic locking ...
 *
 */

