/*
 * 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
 */
package brn.distsim.ormapper.util;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;


import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.hibernate.event.EvictEventListener;
import org.hibernate.event.SaveOrUpdateEventListener;
import org.hibernate.exception.LockAcquisitionException;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.python.core.PyInstance;
import org.python.core.PyProxy;

import brn.distsim.ormapper.class2hbm.MapGenerator;
import brn.distsim.ormapper.class2hbm.ReflectedClass;
import brn.distsim.ormapper.class2hbm.ReflectedProperty;

public class DBSaver extends SessionHandler {



	public static class ReflectionInspector {
		private Set classes = new HashSet();

		private Set objects = new HashSet();

		public void inspect(Object newObject) throws SQLException {
			if (null == newObject)
				return;

			inspect(newObject, newObject.getClass());
		}

		public void inspect(Object newObject, Class newClass)
				throws SQLException {
			if (objects.contains(newObject))
				return;

			classes.add(newClass);
			Class superclass = newClass.getSuperclass();
			if (null != superclass)
				inspect(newObject, superclass);

			// add object after traversal of all super classes
			objects.add(newObject);

			// empty getprops and setprops if a getter and setter pair
			// should come from one class and not a class and its superclass
			try {
				if (newClass.isArray()) {
					int length = Array.getLength(newObject);
					for (int i = 0; i < length; i++)
						inspect(Array.get(newObject, i));
				} else {
					Field[] fields = newClass.getDeclaredFields();

					for (int i = 0; i < fields.length; ++i) {
						Field field = fields[i];
						int modifiers = field.getModifiers();
						// static fields aren't mapped
						if (Modifier.isStatic(modifiers))
							continue;

						field.setAccessible(true);
						inspect(field.get(newObject));
					}
				}
			} catch (SecurityException e) {
				// ignore!?
			} catch (IllegalArgumentException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

		}

		/**
		 * @return the classes
		 */
		public Set getClasses() {
			return classes;
		}
	}



	private EvictEventHandler evictHandler;

	private MapGenerator generator;

	private SaveEventHandler saveHandler;

	private SessionFactory sessionFactory;

	private int counter;

	private Object badInstance;

//	private Set setBadClasses = new HashSet();

	private SimpleDateFormat sqlDate = new SimpleDateFormat(
			"yyyy-MM-dd HH:mm:ss");

	private PreparedStatement expectedEndQuery;

	private PreparedStatement writeXml;

	private List classesNotToMap;

	public void setBadInstance(Object c) {
	  badInstance = c;
//		setBadClasses.add(c);
	}

	public DBSaver(String DBUrl, String DBUser, String DBPasswd, int jobId)
			throws FileNotFoundException, IOException, ClassNotFoundException,
			SQLException {
		super(DBUrl, DBUser, DBPasswd, jobId);
		initialize();
	}

	private void initialize() throws SQLException {
		evictHandler = new EvictEventHandler();
		connection.setAutoCommit(true);
		connection
				.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
		interceptor.setDoDirtyCheck(false);
		expectedEndQuery = connection
				.prepareStatement("UPDATE progress SET expectedEnd = ? WHERE simulationId = ?;");
		writeXml = connection
				.prepareStatement("UPDATE mappings SET mapping = ?;");
		generator = new MapGenerator(null, ClassLoader.getSystemClassLoader());
		saveHandler = new SaveEventHandler(this, generator);
		addClassToMapping(Object.class);
		rebuildSession();
	}

	/**
	 * constructor for testing purposed, will use a new simulation in group 1,
	 * which is by convention the testing group. The definitions DB connection
	 * is needed to create that simulation
	 *
	 * @param resUrl
	 *            URL to results DB
	 * @param resUser
	 *            user in results DB
	 * @param resPasswd
	 *            password in results DB
	 * @param defUrl
	 *            URL to definitions DB
	 * @param defUser
	 *            user in definitions DB
	 * @param defPasswd
	 *            password in definitions DB
	 * @throws SQLException
	 *             if preparing the DBs fails
	 * @throws ClassNotFoundException
	 *             if the MySQL driver isn't found
	 */
	public DBSaver(String resUrl, String resUser, String resPasswd,
			String defUrl, String defUser, String defPasswd)
			throws SQLException, ClassNotFoundException {
		super(resUrl, resUser, resPasswd, defUrl, defUser, defPasswd);
		initialize();
	}

	public synchronized void addToMapping(Object newObject) throws SQLException {
	  if (newObject instanceof PyProxy) {
      PyProxy proxy = (PyProxy) newObject;
      PyInstance pyInstance = proxy._getPyInstance();

      String className = proxy.getClass().getName();

      if (null != classesNotToMap
          && classesNotToMap.contains(className)) {
        return;
      }
      generator.addPyClass(pyInstance, className);
	  } else {
  		ReflectionInspector inspector = new ReflectionInspector();
  		inspector.inspect(newObject);

  		Iterator iter = inspector.getClasses().iterator();
  		while (null != iter && iter.hasNext())
  			addClassToMapping((Class) iter.next());
	  }
		rebuildSession();
	}

	public synchronized void rebuildSession() throws SQLException {
		String xml = generator.getXML();
		Configuration cfg = new CascadeConfiguration().addProperties(hbProps)
				.setInterceptor(interceptor).addXML(xml);
		EvictEventListener[] evicters = { evictHandler };
		cfg.getEventListeners().setEvictEventListeners(evicters);
		SaveOrUpdateEventListener[] savers = { saveHandler };
		cfg.getEventListeners().setSaveOrUpdateEventListeners(savers);
		cfg.getEventListeners().setSaveEventListeners(savers);
		exportSchema(cfg);
		writeXml.setString(1, xml);
		writeXml.execute();

    if (session != null) {
      session.close();
      counter = 0;
    }
    if (sessionFactory != null)
      sessionFactory.close();

    sessionFactory = cfg.buildSessionFactory();
		session = sessionFactory.openSession();
	}

	public synchronized void addClassToMapping(Class newClass)
			throws SQLException {
		if (null != classesNotToMap
				&& classesNotToMap.contains(newClass.getName())) {
//			System.out.println("not mapping class: " + newClass);
			return;
		}
//		System.out.println("class not mapped: " + newClass);
		generator.addClass(newClass.getName(), false);
	}

	public synchronized void addToMapping(Collection newClasses)
			throws SQLException {
		Iterator iter = newClasses.iterator();
		while (null != iter && iter.hasNext()) {
			addClassToMapping((Class) iter.next());
		}
	}

	private void exportSchema(Configuration cfg) throws SQLException {
		SchemaExport schema = new SchemaExport(cfg);
		schema.execute(false, true, false, true);
		Iterator i = generator.getMappedClasses().iterator();
		while (i.hasNext()) {
		  String name = (String) i.next();
      ReflectedClass rc = generator.getReflectedClass(name);
//			Class c = null;
//			try {
//				c = Class.forName(name);
//			} catch (ClassNotFoundException e) {
//				e.printStackTrace();
//			}
			if (!Object.class.getName().equals(name))
				connection.createStatement().executeUpdate(
						"REPLACE INTO class_hierarchy VALUES ('"
								+ rc.getName().replace('.', '_') + "', '"
								+ rc.getSuperclass().replace('.', '_')
								+ "');");
			ReflectedProperty props[] = rc.getProperties();
			for (int j = 0; j < props.length; ++j) {
				String tableName = props[j].getSubTableName();
				if (tableName != null) {
					connection.createStatement().executeUpdate(
							"REPLACE INTO class_members VALUES ('"
									+ rc.getName().replace('.', '_') + "', '"
									+ tableName.replace('.', '_') + "');");
				}
			}
		}

	}

	/**
	 * With this method you can save any Java Object to the database - well,
	 * almost any. Due to limitations in Java and Hibernate you cannot save: -
	 * polymorphic references to java.lang.Class objects. Unfortunately this
	 * means you can'saveTrans save any collections of class objects. -
	 * polymorphic references to arrays (an int[] for example, is actually a
	 * java.lang.Object, but you can'saveTrans save it as such) - arrays of
	 * java.lang.Class objects. All needed database tables are generated on the
	 * fly, so you don'saveTrans need to care about anything in advance - well,
	 * almost. You need to specify the database parameters in the constructor.
	 * Objects will be parsed recursively and all their members and members of
	 * members, etc. will be saved, too. So be careful what you save.
	 *
	 * @throws SQLException
	 */
	public synchronized void save(Object o, String entity, boolean doEvict)
			throws SQLException {
		EntityResolver.setEntity(entity);
		session.flush();
		Transaction saveTrans = session.beginTransaction();
		while (true) {
			try {
				session.save(o);
				saveTrans.commit();
				break;
			} catch (ClassNotMappedException e) {
				saveTrans.rollback();
				addToMapping(badInstance);
				saveTrans = session.beginTransaction();
				badInstance = null;
				continue;
			} catch (LockAcquisitionException e) {
				session.close();
				session = sessionFactory.openSession();
				saveTrans = session.beginTransaction();
        System.err.println(new Date().toString() + " restarting transaction.");
				continue;
			}
		}

		counter = 0;
		if (doEvict)
			session.evict(o);
	}

	/**
	 * This method can be used to save an object without catching unmapped
	 * classes. It's much faster than save() because only one transactions is
	 * used for all objects, but it will throw an Exception and leave the
	 * database in an inconsistent state if an unmapped class is encountered.
	 *
	 * @param o
	 *            Object to be saved
	 * @throws ClassNotMappedException
	 *             if an unmapped class is found
	 */
	public synchronized void quickSave(Object o, String entity, boolean doEvict)
			throws ClassNotMappedException {
		counter++;
		EntityResolver.setEntity(entity);
		session.save(o);

		if (doEvict) {
			session.flush();
			session.evict(o);
		}

		if (counter > 50) {
			session.flush();
			session.clear();
			counter = 0;
		}
	}

	public void setExpectedEnd(int timestamp) {
		try {
			expectedEndQuery.setInt(1, simulationId);
			expectedEndQuery.setString(2, sqlDate.format(new Date(timestamp)));
			expectedEndQuery.execute();
			connection.commit();
		} catch (SQLException e) {
			// not that important
			e.printStackTrace();
		}

	}

	/**
	 * @param classesNotToMap
	 *            the classesNotToMap to set
	 */
	public void setClassesNotToMap(String[] classesNotToMap) {
		this.classesNotToMap = Arrays.asList(classesNotToMap);
	}

  public void close() {
    try {
      if (null != writeXml)
        writeXml.close();
    } catch (SQLException e1) {}
    try {
      if (session != null)
        session.flush();
    } catch (Throwable e) {}
    try {
      if (session != null)
        session.close();
    } catch (Throwable e) {}
    try {
      if (sessionFactory != null)
        sessionFactory.close();
    } catch (Throwable e) {}

    writeXml = null;
    session = null;
    sessionFactory = null;
    super.close();
  }

  public void finalize() throws Throwable {
    close();
    super.finalize();
	}
}
