/* Copyright 2004-2005 the original author or authors.
 *
 * Licensed 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.codehaus.groovy.grails.orm.hibernate;

import groovy.lang.GroovySystem;
import groovy.lang.MetaClassRegistry;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.groovy.grails.commons.GrailsApplication;
import org.codehaus.groovy.grails.orm.hibernate.cfg.DefaultGrailsDomainConfiguration;
import org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsDomainConfiguration;
import org.codehaus.groovy.grails.orm.hibernate.events.SaveOrUpdateEventListener;
import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.SessionFactory;
import org.hibernate.cache.CacheException;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.event.*;
import org.hibernate.metadata.ClassMetadata;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.orm.hibernate3.LocalSessionFactoryBean;

/**
 * A SessionFactory bean that allows the configuration class to
 * be changed and customise for usage within Grails.
 *
 * @author Graeme Rocher
 * @since 07-Jul-2005
 */
public class ConfigurableLocalSessionFactoryBean extends
        LocalSessionFactoryBean implements ApplicationContextAware {

    private static final Log LOG = LogFactory.getLog(ConfigurableLocalSessionFactoryBean.class);
    private ClassLoader classLoader = null;
    private GrailsApplication grailsApplication;
    private Class<?> configClass;
    private Class<?> currentSessionContextClass;
    private HibernateEventListeners hibernateEventListeners;

    /**
     * Sets class to be used for the Hibernate CurrentSessionContext
     *
     * @param currentSessionContextClass An implementation of the CurrentSessionContext interface
     */
    public void setCurrentSessionContextClass(Class<?> currentSessionContextClass) {
        this.currentSessionContextClass = currentSessionContextClass;
    }

    /**
     * Sets the class to be used for Hibernate Configuration
     * @param configClass A subclass of the Hibernate Configuration class
     */
    public void setConfigClass(Class<?> configClass) {
        this.configClass = configClass;
    }

    /**
     * @return Returns the grailsApplication.
     */
    public GrailsApplication getGrailsApplication() {
        return grailsApplication;
    }

    /**
     * @param grailsApplication The grailsApplication to set.
     */
    public void setGrailsApplication(GrailsApplication grailsApplication) {
        this.grailsApplication = grailsApplication;
    }

    /**
     * Overrides default behaviour to allow for a configurable configuration class.
     */
     @Override
    protected Configuration newConfiguration() {
        ClassLoader cl = this.classLoader != null ? this.classLoader : Thread.currentThread().getContextClassLoader();
        if (configClass == null) {
            try {
                configClass = cl.loadClass("org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsAnnotationConfiguration");
            }
            catch (Throwable e) {
                // probably not Java 5 or missing some annotation jars, use default
                configClass = DefaultGrailsDomainConfiguration.class;
            }
        }
        Object config = BeanUtils.instantiateClass(configClass);
        if (config instanceof GrailsDomainConfiguration) {
            ((GrailsDomainConfiguration)config).setGrailsApplication(grailsApplication);
        }
        if (currentSessionContextClass != null) {
            ((Configuration)config).setProperty(Environment.CURRENT_SESSION_CONTEXT_CLASS, currentSessionContextClass.getName());
            // don't allow Spring's LocaalSessionFactoryBean to override setting
            setExposeTransactionAwareSessionFactory(false);
        }
        return (Configuration)config;
    }

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

    @Override
    protected SessionFactory newSessionFactory(Configuration config) throws HibernateException {
        try {
            return super.newSessionFactory(config);
        }
        catch (HibernateException e) {
            Throwable cause = e.getCause();
            if (isCacheConfigurationError(cause)) {
                LOG.fatal("There was an error configuring the Hibernate second level cache: " + getCauseMessage(e));
                LOG.fatal("This is normally due to one of two reasons. Either you have incorrectly specified the cache provider class name in [DataSource.groovy] or you do not have the cache provider on your classpath (eg. runtime (\"net.sf.ehcache:ehcache:1.6.1\"))");
                if (grails.util.Environment.isDevelopmentMode()) {
                    System.exit(1);
                }
            }
            throw e;
        }
    }

    private String getCauseMessage(HibernateException e) {
        Throwable cause = e.getCause();
        if (cause instanceof InvocationTargetException) {
            cause = ((InvocationTargetException)cause).getTargetException();
        }
        return cause.getMessage();
    }

    private boolean isCacheConfigurationError(Throwable cause) {
        if (cause instanceof InvocationTargetException) {
            cause = ((InvocationTargetException)cause).getTargetException();
        }
        return cause != null && (cause instanceof CacheException);
    }

    @Override
    public void destroy() throws HibernateException {
        MetaClassRegistry registry = GroovySystem.getMetaClassRegistry();
        Map<?, ?> classMetaData = getSessionFactory().getAllClassMetadata();
        for (Object o : classMetaData.values()) {
            ClassMetadata classMetadata = (ClassMetadata) o;
            Class<?> mappedClass = classMetadata.getMappedClass(EntityMode.POJO);
            registry.removeMetaClass(mappedClass);
        }
        super.destroy();
    }

    /**
     * Merge HibernateEventListeners with the default ones
     */
    @Override
    protected void postProcessConfiguration(final Configuration config) throws HibernateException {
        super.postProcessConfiguration(config);
        if (hibernateEventListeners == null || hibernateEventListeners.getListenerMap() == null) {
            return;
        }

        EventListeners listeners = config.getEventListeners();
        Map<String,Object> listenerMap = hibernateEventListeners.getListenerMap();            
        addNewListenerToConfiguration(config, "auto-flush", AutoFlushEventListener.class, 
                listeners.getAutoFlushEventListeners(), listenerMap);
        addNewListenerToConfiguration(config, "merge", MergeEventListener.class, 
                listeners.getMergeEventListeners(), listenerMap);
        addNewListenerToConfiguration(config, "create", PersistEventListener.class, 
                listeners.getPersistEventListeners(), listenerMap);
        addNewListenerToConfiguration(config, "create-onflush", PersistEventListener.class, 
                listeners.getPersistOnFlushEventListeners(), listenerMap);
        addNewListenerToConfiguration(config, "delete", DeleteEventListener.class, 
                listeners.getDeleteEventListeners(), listenerMap);
        addNewListenerToConfiguration(config, "dirty-check", DirtyCheckEventListener.class, 
                listeners.getDirtyCheckEventListeners(), listenerMap);
        addNewListenerToConfiguration(config, "evict", EvictEventListener.class, 
                listeners.getEvictEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "flush", FlushEventListener.class, 
                listeners.getFlushEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "flush-entity", FlushEntityEventListener.class, 
                listeners.getFlushEntityEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "load", LoadEventListener.class, 
                listeners.getLoadEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "load-collection", InitializeCollectionEventListener.class, 
                listeners.getInitializeCollectionEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "lock", LockEventListener.class, 
                listeners.getLockEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "refresh", RefreshEventListener.class, 
                listeners.getRefreshEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "replicate", ReplicateEventListener.class, 
                listeners.getReplicateEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "save-update", SaveOrUpdateEventListener.class, 
                listeners.getSaveOrUpdateEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "save", SaveOrUpdateEventListener.class, 
                listeners.getSaveEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "update", SaveOrUpdateEventListener.class, 
                listeners.getUpdateEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "pre-load", PreLoadEventListener.class, 
                listeners.getPreLoadEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "pre-update", PreUpdateEventListener.class, 
                listeners.getPreUpdateEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "pre-delete", PreDeleteEventListener.class, 
                listeners.getPreDeleteEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "pre-insert", PreInsertEventListener.class, 
                listeners.getPreInsertEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "pre-collection-recreate", PreCollectionRecreateEventListener.class, 
                listeners.getPreCollectionRecreateEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "pre-collection-remove", PreCollectionRemoveEventListener.class, 
                listeners.getPreCollectionRemoveEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "pre-collection-update", PreCollectionUpdateEventListener.class, 
                listeners.getPreCollectionUpdateEventListeners(), listenerMap);
        addNewListenerToConfiguration(config, "post-load", PostLoadEventListener.class, 
                listeners.getPostLoadEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "post-update", PostUpdateEventListener.class, 
                listeners.getPostUpdateEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "post-delete", PostDeleteEventListener.class, 
                listeners.getPostDeleteEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "post-insert", PostInsertEventListener.class, 
                listeners.getPostInsertEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "post-commit-update", PostUpdateEventListener.class, 
                listeners.getPostCommitUpdateEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "post-commit-delete", PostDeleteEventListener.class, 
                listeners.getPostCommitDeleteEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "post-commit-insert", PostInsertEventListener.class, 
                listeners.getPostCommitInsertEventListeners(), listenerMap);            
        addNewListenerToConfiguration(config, "post-collection-recreate", PostCollectionRecreateEventListener.class, 
                listeners.getPostCollectionRecreateEventListeners(), listenerMap);        
        addNewListenerToConfiguration(config, "post-collection-remove", PostCollectionRemoveEventListener.class, 
                listeners.getPostCollectionRemoveEventListeners(), listenerMap);        
        addNewListenerToConfiguration(config, "post-collection-update", PostCollectionUpdateEventListener.class, 
                listeners.getPostCollectionUpdateEventListeners(), listenerMap);
    }

    @SuppressWarnings("unchecked")
    private <T> void addNewListenerToConfiguration(final Configuration config, final String listenerType,
            final Class<? extends T> klass, final T[] currentListeners, final Map<String,Object> newlistenerMap) {

        Object newListener = newlistenerMap.get(listenerType);
        if (newListener == null) return;

        if (currentListeners != null && currentListeners.length > 0) {
            T[] newListeners = (T[])Array.newInstance(klass, currentListeners.length + 1);
            System.arraycopy(currentListeners, 0, newListeners, 0, currentListeners.length);
            newListeners[currentListeners.length] = (T)newListener;
            config.setListeners(listenerType, newListeners);
        }
        else {
            config.setListener(listenerType, newListener);
        }
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // not used
    }

    public void setHibernateEventListeners(final HibernateEventListeners listeners) {
        hibernateEventListeners = listeners;
    }
}
