/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 *
 * The Apereo Foundation licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
*/
package org.unitime.commons.hibernate.util;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.sql.Connection;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import javax.naming.spi.NamingManager;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.MappingException;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.MySQLDialect;
import org.hibernate.dialect.Oracle8iDialect;
import org.hibernate.dialect.function.SQLFunctionTemplate;
import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.internal.util.ConfigHelper;
import org.hibernate.mapping.Formula;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.Selectable;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.type.IntegerType;
import org.unitime.commons.LocalContext;
import org.unitime.commons.hibernate.connection.DisposableConnectionProvider;
import org.unitime.commons.hibernate.id.UniqueIdGenerator;
import org.unitime.timetable.ApplicationProperties;
import org.unitime.timetable.defaults.ApplicationProperty;
import org.unitime.timetable.model.base._BaseRootDAO;
import org.unitime.timetable.model.dao._RootDAO;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;

/**
 * @author Tomas Muller
 */
public class HibernateUtil {
    private static Log sLog = LogFactory.getLog(HibernateUtil.class);
    private static SessionFactory sSessionFactory = null;

	private static void setProperty(org.w3c.dom.Document document, String name, String value) {
        if (value==null) {
            removeProperty(document, name);
        } else {
            org.w3c.dom.Element hibConfiguration = (org.w3c.dom.Element)document.getElementsByTagName("hibernate-configuration").item(0);
            org.w3c.dom.Element sessionFactoryConfig = (org.w3c.dom.Element)hibConfiguration.getElementsByTagName("session-factory").item(0);
            NodeList properties = sessionFactoryConfig.getElementsByTagName("property");
            for (int i=0;i<properties.getLength();i++) {
                org.w3c.dom.Element property = (org.w3c.dom.Element)properties.item(i);
                if (name.equals(property.getAttribute("name"))) {
                    Text text = (Text)property.getFirstChild();
                    if (text==null) {
                        property.appendChild(document.createTextNode(value));
                    } else {
                        text.setData(value);
                    }
                    return;
                }
            }
            org.w3c.dom.Element property = document.createElement("property");
            property.setAttribute("name",name);
            property.appendChild(document.createTextNode(value));
            sessionFactoryConfig.appendChild(property);
        }
	}
	
	private static void removeProperty(org.w3c.dom.Document document, String name) {
		org.w3c.dom.Element hibConfiguration = (org.w3c.dom.Element)document.getElementsByTagName("hibernate-configuration").item(0);
		org.w3c.dom.Element sessionFactoryConfig = (org.w3c.dom.Element)hibConfiguration.getElementsByTagName("session-factory").item(0);
		org.w3c.dom.NodeList properties = sessionFactoryConfig.getElementsByTagName("property");
        for (int i=0;i<properties.getLength();i++) {
        	org.w3c.dom.Element property = (org.w3c.dom.Element)properties.item(i);
        	if (name.equals(property.getAttribute("name"))) {
        		sessionFactoryConfig.removeChild(property);
        		return;
        	}
        }
	}
    
    public static void configureHibernate(String connectionUrl) throws Exception {
        Properties properties = ApplicationProperties.getProperties();
        properties.setProperty("connection.url", connectionUrl);
        configureHibernate(properties);
    }
    
	public static String getProperty(Properties properties, String name) {
        String value = properties.getProperty(name);
        if (value!=null) {
            sLog.debug("   -- " + name + "=" + value);
        	return value;
        }
        sLog.debug("   -- using application properties for " + name);
        value = ApplicationProperties.getProperty(name);
        sLog.debug("     -- " + name + "=" + value);
        return value;
    }
    
    public static void fixSchemaInFormulas(Configuration cfg) {
    	cfg.buildMappings();
        String schema = cfg.getProperty("default_schema"); 
        if (schema!=null) {
            for (Iterator i=cfg.getClassMappings();i.hasNext();) {
                PersistentClass pc = (PersistentClass)i.next();
                for (Iterator j=pc.getPropertyIterator();j.hasNext();) {
                    Property p = (Property)j.next();
                    for (Iterator k=p.getColumnIterator();k.hasNext();) {
                        Selectable c = (Selectable)k.next();
                        if (c instanceof Formula) {
                            Formula f = (Formula)c;
                            if (f.getFormula()!=null && f.getFormula().indexOf("%SCHEMA%")>=0) {
                                f.setFormula(f.getFormula().replaceAll("%SCHEMA%", schema));
                                sLog.debug("Schema updated in "+pc.getClassName()+"."+p.getName()+" to "+f.getFormula());
                            }
                        }
                    }
                }
            }
        }
    }

	public static void configureHibernate(Properties properties) throws Exception {
		if (sSessionFactory!=null) {
			sSessionFactory.close();
			sSessionFactory=null;
		}
		
		if (!NamingManager.hasInitialContextFactoryBuilder())
			NamingManager.setInitialContextFactoryBuilder(new LocalContext(null));
		
		sLog.info("Connecting to "+getProperty(properties,"connection.url"));
		ClassLoader classLoader = HibernateUtil.class.getClassLoader();
		sLog.debug("  -- class loader retrieved");

		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		sLog.debug("  -- document factory created");
        DocumentBuilder builder = factory.newDocumentBuilder();
        builder.setEntityResolver(new EntityResolver() {
    	    public InputSource resolveEntity(String publicId, String systemId) {
    	        if (publicId.equals("-//Hibernate/Hibernate Mapping DTD 3.0//EN")) {
    	            return new InputSource(HibernateUtil.class.getClassLoader().getResourceAsStream("org/hibernate/hibernate-mapping-3.0.dtd"));
    	        } else if (publicId.equals("-//Hibernate/Hibernate Mapping DTD//EN")) {
        	            return new InputSource(HibernateUtil.class.getClassLoader().getResourceAsStream("org/hibernate/hibernate-mapping-3.0.dtd"));
    	        } else if (publicId.equals("-//Hibernate/Hibernate Configuration DTD 3.0//EN")) {
    	            return new InputSource(HibernateUtil.class.getClassLoader().getResourceAsStream("org/hibernate/hibernate-configuration-3.0.dtd"));
    	        } else if (publicId.equals("-//Hibernate/Hibernate Configuration DTD//EN")) {
    	            return new InputSource(HibernateUtil.class.getClassLoader().getResourceAsStream("org/hibernate/hibernate-configuration-3.0.dtd"));
        	    }
    	        return null;
    	    }
    	});
        sLog.debug("  -- document builder created");
        Document document = builder.parse(classLoader.getResource("hibernate.cfg.xml").openStream());
        sLog.debug("  -- hibernate.cfg.xml parsed");
        
        String dialect = getProperty(properties, "dialect");
        if (dialect!=null)
        	setProperty(document, "dialect", dialect);

        String idgen = getProperty(properties, "tmtbl.uniqueid.generator");
        if (idgen!=null)
            setProperty(document, "tmtbl.uniqueid.generator", idgen);

        if (ApplicationProperty.HibernateClusterEnabled.isFalse())
        	setProperty(document, "net.sf.ehcache.configurationResourceName", "ehcache-nocluster.xml");

        // Remove second level cache
        setProperty(document, "hibernate.cache.use_second_level_cache", "false");
        setProperty(document, "hibernate.cache.use_query_cache", "false");
        removeProperty(document, "hibernate.cache.region.factory_class");

        for (Enumeration e=properties.propertyNames();e.hasMoreElements();) {
            String name = (String)e.nextElement();
            if (name.startsWith("hibernate.") || name.startsWith("connection.") || name.startsWith("tmtbl.hibernate.")) {
				String value = properties.getProperty(name);
                if ("NULL".equals(value))
                    removeProperty(document, name);
                else
                    setProperty(document, name, value);
                if (!name.equals("connection.password"))
                    sLog.debug("  -- set "+name+": "+value);
                else
                    sLog.debug("  -- set "+name+": *****");
            }
        }

        String default_schema = getProperty(properties, "default_schema");
        if (default_schema!=null)
            setProperty(document, "default_schema", default_schema);

        sLog.debug("  -- hibernate.cfg.xml altered");
        
        Configuration cfg = new Configuration();
        sLog.debug("  -- configuration object created");
        
    	cfg.setEntityResolver(new EntityResolver() {
    	    public InputSource resolveEntity(String publicId, String systemId) {
    	        if (publicId.equals("-//Hibernate/Hibernate Mapping DTD 3.0//EN")) {
    	            return new InputSource(HibernateUtil.class.getClassLoader().getResourceAsStream("org/hibernate/hibernate-mapping-3.0.dtd"));
    	        } else if (publicId.equals("-//Hibernate/Hibernate Mapping DTD//EN")) {
        	            return new InputSource(HibernateUtil.class.getClassLoader().getResourceAsStream("org/hibernate/hibernate-mapping-3.0.dtd"));
    	        } else if (publicId.equals("-//Hibernate/Hibernate Configuration DTD 3.0//EN")) {
    	            return new InputSource(HibernateUtil.class.getClassLoader().getResourceAsStream("org/hibernate/hibernate-configuration-3.0.dtd"));
    	        } else if (publicId.equals("-//Hibernate/Hibernate Configuration DTD//EN")) {
    	            return new InputSource(HibernateUtil.class.getClassLoader().getResourceAsStream("org/hibernate/hibernate-configuration-3.0.dtd"));
        	    }
    	        return null;
    	    }
    	});
        sLog.debug("  -- added entity resolver");
        
        cfg.configure(document);
        sLog.debug("  -- hibernate configured");

        fixSchemaInFormulas(cfg);
        
        UniqueIdGenerator.configure(cfg);
        
        (new _BaseRootDAO() {
    		void setConf(Configuration cfg) {
    			_BaseRootDAO.sConfiguration = cfg;
    		}
    		protected Class getReferenceClass() { return null; }
    	}).setConf(cfg);
        sLog.debug("  -- configuration set to _BaseRootDAO");

        sSessionFactory = cfg.buildSessionFactory();
        sLog.debug("  -- session factory created");
        
        (new _BaseRootDAO() {
    		void setSF(SessionFactory fact) {
    			_BaseRootDAO.sSessionFactory = fact;
    		}
    		protected Class getReferenceClass() { return null; }
    	}).setSF(sSessionFactory);
        sLog.debug("  -- session factory set to _BaseRootDAO");
        
        addBitwiseOperationsToDialect();
        sLog.debug("  -- bitwise operation added to the dialect if needed");
        
        DatabaseUpdate.update();
    }
    
    public static void closeHibernate() {
		if (sSessionFactory!=null) {
			if (sSessionFactory instanceof SessionFactoryImpl) {
				ConnectionProvider cp = ((SessionFactoryImpl)sSessionFactory).getConnectionProvider();
				if (cp instanceof DisposableConnectionProvider) {
					try {
						((DisposableConnectionProvider)cp).destroy();
					} catch (Exception e) {
						sLog.error("Failed to destroy connection provider: " + e.getMessage());
					}
				}
			}
			sSessionFactory.close();
			sSessionFactory=null;
		}
	}
    
    public static void configureHibernateFromRootDAO(String cfgName, Configuration cfg) {
        try {
        	EntityResolver entityResolver = new EntityResolver() {
        	    public InputSource resolveEntity(String publicId, String systemId) {
        	        if (publicId.equals("-//Hibernate/Hibernate Mapping DTD 3.0//EN")) {
        	            return new InputSource(HibernateUtil.class.getClassLoader().getResourceAsStream("org/hibernate/hibernate-mapping-3.0.dtd"));
        	        } else if (publicId.equals("-//Hibernate/Hibernate Mapping DTD//EN")) {
            	            return new InputSource(HibernateUtil.class.getClassLoader().getResourceAsStream("org/hibernate/hibernate-mapping-3.0.dtd"));
        	        } else if (publicId.equals("-//Hibernate/Hibernate Configuration DTD 3.0//EN")) {
        	            return new InputSource(HibernateUtil.class.getClassLoader().getResourceAsStream("org/hibernate/hibernate-configuration-3.0.dtd"));
        	        } else if (publicId.equals("-//Hibernate/Hibernate Configuration DTD//EN")) {
        	            return new InputSource(HibernateUtil.class.getClassLoader().getResourceAsStream("org/hibernate/hibernate-configuration-3.0.dtd"));
            	    }
        	        return null;
        	    }
        	};
        	
        	cfg.setEntityResolver(entityResolver);
            sLog.debug("  -- added entity resolver");

            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            sLog.debug("  -- document factory created");
            DocumentBuilder builder = factory.newDocumentBuilder();
            builder.setEntityResolver(entityResolver);
            sLog.debug("  -- document builder created");
            BufferedInputStream inputStream = (BufferedInputStream) ConfigHelper.getConfigStream(cfgName==null?"hibernate.cfg.xml":cfgName);
            Document document = builder.parse(inputStream);
            
            String dialect = ApplicationProperty.DatabaseDialect.value();
            if (dialect!=null) setProperty(document, "dialect", dialect);
            
            String default_schema = ApplicationProperty.DatabaseSchema.value();
            if (default_schema!=null) setProperty(document, "default_schema", default_schema);
            
            String idgen = ApplicationProperty.DatabaseUniqueIdGenerator.value();
            if (idgen!=null) setProperty(document, "tmtbl.uniqueid.generator", idgen);
            
            if (ApplicationProperty.HibernateClusterEnabled.isFalse())
            	setProperty(document, "net.sf.ehcache.configurationResourceName", "ehcache-nocluster.xml");
            
            for (Enumeration e=ApplicationProperties.getProperties().propertyNames();e.hasMoreElements();) {
                String name = (String)e.nextElement();
                if (name.startsWith("hibernate.") || name.startsWith("connection.") || name.startsWith("tmtbl.hibernate.")) {
					String value = ApplicationProperties.getProperty(name);
                    if ("NULL".equals(value))
                        removeProperty(document, name);
                    else
                        setProperty(document, name, value);
                    if (!name.equals("connection.password"))
                        sLog.debug("  -- set "+name+": "+value);
                    else
                        sLog.debug("  -- set "+name+": *****");
                }
            }

            cfg.configure(document);
            sLog.debug("  -- hibernate configured");
            
            HibernateUtil.fixSchemaInFormulas(cfg);
            sLog.debug("  -- %SCHEMA% in formulas changed to "+cfg.getProperty("default_schema"));
            
            UniqueIdGenerator.configure(cfg);
            sLog.debug("  -- UniquId generator configured");
        } catch (Exception e) {
            sLog.error("Unable to configure hibernate, reason: "+e.getMessage(),e);
        }
    }
    
    private static String sConnectionUrl = null;
    
    public static String getConnectionUrl() {
        if (sConnectionUrl==null) {
            try {
                SessionImplementor session = (SessionImplementor)new _RootDAO().getSession();
                Connection connection = session.getJdbcConnectionAccess().obtainConnection();
                sConnectionUrl = connection.getMetaData().getURL();
                session.getJdbcConnectionAccess().releaseConnection(connection);
            } catch (Exception e) {
                sLog.error("Unable to get connection string, reason: "+e.getMessage(),e);
            }
        }
        return sConnectionUrl;
    }

    public static String getDatabaseName() {
        String schema = _RootDAO.getConfiguration().getProperty("default_schema");
        String url = getConnectionUrl();
        if (url==null) return "N/A";
        if (url.startsWith("jdbc:oracle:")) {
            return schema+"@"+url.substring(1+url.lastIndexOf(':'));
        }
        return schema;
    }
    
    public static void clearCache() {
        clearCache(null, true);
    }
    
    public static void clearCache(Class persistentClass) {
        clearCache(persistentClass, false);
    }

    public static void clearCache(Class persistentClass, boolean evictQueries) {
        _RootDAO dao = new _RootDAO();
        org.hibernate.Session hibSession = dao.getSession(); 
        SessionFactory hibSessionFactory = hibSession.getSessionFactory();
        if (persistentClass==null) {
            for (Iterator i=hibSessionFactory.getAllClassMetadata().entrySet().iterator();i.hasNext();) {
                Map.Entry entry = (Map.Entry)i.next();
                String className = (String)entry.getKey();
                ClassMetadata classMetadata = (ClassMetadata)entry.getValue();
                try {
                    hibSessionFactory.getCache().evictEntityRegion(Class.forName(className));
                    for (int j=0;j<classMetadata.getPropertyNames().length;j++) {
                        if (classMetadata.getPropertyTypes()[j].isCollectionType()) {
                            try {
                                hibSessionFactory.getCache().evictCollectionRegion(className+"."+classMetadata.getPropertyNames()[j]);
                            } catch (MappingException e) {}
                        }
                    }
                } catch (ClassNotFoundException e) {}
            }
            hibSessionFactory.getCache().evictEntityRegions();
            hibSessionFactory.getCache().evictCollectionRegions();
        } else {
            ClassMetadata classMetadata = hibSessionFactory.getClassMetadata(persistentClass);
            hibSessionFactory.getCache().evictEntityRegion(persistentClass);
            if (classMetadata!=null) {
                for (int j=0;j<classMetadata.getPropertyNames().length;j++) {
                    if (classMetadata.getPropertyTypes()[j].isCollectionType()) {
                        try {
                            hibSessionFactory.getCache().evictCollectionRegion(persistentClass.getClass().getName()+"."+classMetadata.getPropertyNames()[j]);
                        } catch (MappingException e) {}
                    }
                }
            }
        }
        if (evictQueries) {
            hibSessionFactory.getCache().evictQueryRegions();
            hibSessionFactory.getCache().evictDefaultQueryRegion();
        }
    }
    
    public static Class<?> getDialect() {
    	try {
    		return Class.forName(_RootDAO.getConfiguration().getProperty("dialect"));
    	} catch (ClassNotFoundException e) {
    		return null;
    	}
    }
    
    public static boolean isMySQL() {
    	return MySQLDialect.class.isAssignableFrom(getDialect());
    }
    
    public static boolean isOracle() {
    	return Oracle8iDialect.class.isAssignableFrom(getDialect());
    }
    
    public static String addDate(String dateSQL, String incrementSQL) {
        if (isMySQL())
            return "adddate("+dateSQL+","+incrementSQL+")";
        else
            return dateSQL+(incrementSQL.startsWith("+")||incrementSQL.startsWith("-")?"":"+")+incrementSQL;
    }
    
    public static String dayOfWeek(String field) {
    	if (isOracle())
    		return "(trunc(" + field + ") - trunc(" + field + ", 'IW'))";
    	else
    		return "weekday(" + field + ")";
    }
    
    public static String date(Date date) {
    	if (isOracle())
    		return "to_date('" + new SimpleDateFormat("yyyy-MM-dd").format(date) + "', 'YYYY-MM-DD')";
    	else
    		return "str_to_date('" + new SimpleDateFormat("yyyy-MM-dd").format(date) + "', '%Y-%m-%d')";
    }
    
    public static void addBitwiseOperationsToDialect() {
    	SessionFactoryImplementor hibSessionFactory = (SessionFactoryImplementor)new _RootDAO().getSession().getSessionFactory();
    	Dialect dialect = hibSessionFactory.getDialect();
    	if (!dialect.getFunctions().containsKey("bit_and")) {
    		if (isOracle())
    			dialect.getFunctions().put("bit_and", new StandardSQLFunction("bitand", IntegerType.INSTANCE));  
    		else
    			dialect.getFunctions().put("bit_and", new SQLFunctionTemplate(IntegerType.INSTANCE, "?1 & ?2"));
    	}
    }
}