/* Copyright (C) 2014 SpringSource
 *
 * 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
 *
 *      https://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 grails.orm.bootstrap

import javax.sql.DataSource

import groovy.transform.CompileStatic

import org.springframework.beans.factory.support.BeanDefinitionRegistry
import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.support.GenericApplicationContext
import org.springframework.core.env.ConfigurableEnvironment
import org.springframework.core.env.PropertyResolver
import org.springframework.transaction.PlatformTransactionManager

import org.grails.datastore.gorm.bootstrap.AbstractDatastoreInitializer
import org.grails.datastore.gorm.jdbc.connections.CachedDataSourceConnectionSourceFactory
import org.grails.datastore.gorm.support.AbstractDatastorePersistenceContextInterceptor
import org.grails.datastore.mapping.config.DatastoreServiceMethodInvokingFactoryBean
import org.grails.datastore.mapping.core.connections.AbstractConnectionSources
import org.grails.datastore.mapping.core.connections.ConnectionSource
import org.grails.datastore.mapping.reflect.ClassUtils
import org.grails.orm.hibernate.HibernateDatastore
import org.grails.orm.hibernate.cfg.Settings
import org.grails.orm.hibernate.connections.HibernateConnectionSourceFactory
import org.grails.orm.hibernate.proxy.HibernateProxyHandler
import org.grails.orm.hibernate.support.HibernateDatastoreConnectionSourcesRegistrar

/**
 * Class that handles the details of initializing GORM for Hibernate
 *
 * @author Graeme Rocher
 * @since 3.0
 */
class HibernateDatastoreSpringInitializer extends AbstractDatastoreInitializer {

    public static final String SESSION_FACTORY_BEAN_NAME = 'sessionFactory'
    public static final String DEFAULT_DATA_SOURCE_NAME = Settings.SETTING_DATASOURCE
    public static final String DATA_SOURCES = Settings.SETTING_DATASOURCES
    public static final String TEST_DB_URL = 'jdbc:h2:mem:grailsDb;LOCK_TIMEOUT=10000;DB_CLOSE_DELAY=-1'

    String defaultDataSourceBeanName = ConnectionSource.DEFAULT
    String defaultSessionFactoryBeanName = SESSION_FACTORY_BEAN_NAME
    Set<String> dataSources = [defaultDataSourceBeanName] as Set<String>
    boolean enableReload = false
    boolean grailsPlugin = false

    HibernateDatastoreSpringInitializer(PropertyResolver configuration, Collection<Class> persistentClasses) {
        super(configuration, persistentClasses)
        configureDataSources(configuration)
    }

    HibernateDatastoreSpringInitializer(PropertyResolver configuration, Class... persistentClasses) {
        super(configuration, persistentClasses)
        configureDataSources(configuration)
    }

    HibernateDatastoreSpringInitializer(PropertyResolver configuration, String... packages) {
        super(configuration, packages)
        configureDataSources(configuration)
    }

    HibernateDatastoreSpringInitializer(Map configuration, Class... persistentClasses) {
        super(configuration, persistentClasses)
        configureDataSources(this.configuration)
    }

    HibernateDatastoreSpringInitializer(Map configuration, Collection<Class> persistentClasses) {
        super(configuration, persistentClasses)
        configureDataSources(this.configuration)
    }

    @CompileStatic
    void configureDataSources(PropertyResolver config) {

        Set<String> dataSourceNames = new HashSet<String>()

        if (config == null) {
            dataSourceNames = [defaultDataSourceBeanName] as Set
        }
        else {
            Map dataSources = config.getProperty(DATA_SOURCES, Map, Collections.emptyMap())

            if (dataSources != null && !dataSources.isEmpty()) {
                dataSourceNames.addAll(AbstractConnectionSources.toValidConnectionSourceNames(dataSources))
            }
            Map dataSource = (Map) config.getProperty(DEFAULT_DATA_SOURCE_NAME, Map, Collections.emptyMap())
            if (dataSource != null && !dataSource.isEmpty()) {
                dataSourceNames.add(ConnectionSource.DEFAULT)
            }
        }
        this.dataSources = dataSourceNames
    }

    @Override
    protected Class<AbstractDatastorePersistenceContextInterceptor> getPersistenceInterceptorClass() {
        getClass().classLoader.loadClass('org.grails.plugin.hibernate.support.HibernatePersistenceContextInterceptor')
    }

    /**
     * Configures an in-memory test data source, don't use in production
     */
    @Override
    ApplicationContext configure() {
        GenericApplicationContext applicationContext = createApplicationContext()
        configureForBeanDefinitionRegistry(applicationContext)
        applicationContext.refresh()
        return applicationContext
    }

    protected String getTestDbUrl() {
        TEST_DB_URL
    }

    @CompileStatic
    ApplicationContext configureForDataSource(DataSource dataSource) {
        GenericApplicationContext applicationContext = createApplicationContext()
        applicationContext.beanFactory.registerSingleton(DEFAULT_DATA_SOURCE_NAME, dataSource)
        configureForBeanDefinitionRegistry(applicationContext)
        applicationContext.refresh()
        return applicationContext
    }

    Closure getBeanDefinitions(BeanDefinitionRegistry beanDefinitionRegistry) {
        ApplicationEventPublisher eventPublisher = super.findEventPublisher(beanDefinitionRegistry)
        Closure beanDefinitions = {
            def common = getCommonConfiguration(beanDefinitionRegistry, 'hibernate')
            common.delegate = delegate
            common.call()

            // for unwrapping / inspecting proxies
            hibernateProxyHandler(HibernateProxyHandler)

            def config = this.configuration
            final boolean isGrailsPresent = isGrailsPresent()
            dataSourceConnectionSourceFactory(CachedDataSourceConnectionSourceFactory)
            hibernateConnectionSourceFactory(HibernateConnectionSourceFactory, persistentClasses as Class[]) { bean ->
                bean.autowire = true
                dataSourceConnectionSourceFactory = ref('dataSourceConnectionSourceFactory')
            }
            hibernateDatastore(HibernateDatastore, config, hibernateConnectionSourceFactory, eventPublisher) { bean ->
                bean.primary = true
            }
            sessionFactory(hibernateDatastore: 'getSessionFactory') { bean ->
                bean.primary = true
            }
            transactionManager(hibernateDatastore: 'getTransactionManager') { bean ->
                bean.primary = true
            }
            autoTimestampEventListener(hibernateDatastore: 'getAutoTimestampEventListener')
            getBeanDefinition('transactionManager').beanClass = PlatformTransactionManager
            hibernateDatastoreConnectionSourcesRegistrar(HibernateDatastoreConnectionSourcesRegistrar, dataSources)
            // domain model mapping context, used for configuration
            grailsDomainClassMappingContext(hibernateDatastore: 'getMappingContext')

            loadDataServices(null)
                    .each { serviceName, serviceClass ->
                        "$serviceName"(DatastoreServiceMethodInvokingFactoryBean, serviceClass) {
                            targetObject = ref('hibernateDatastore')
                            targetMethod = 'getService'
                            arguments = [serviceClass]
                        }
                    }

            if (isGrailsPresent) {
                if (ClassUtils.isPresent('org.grails.plugin.hibernate.support.AggregatePersistenceContextInterceptor')) {
                    ClassLoader cl = ClassUtils.getClassLoader()
                    persistenceInterceptor(cl.loadClass('org.grails.plugin.hibernate.support.AggregatePersistenceContextInterceptor'), ref('hibernateDatastore'))
                    proxyHandler(cl.loadClass('org.grails.datastore.gorm.proxy.ProxyHandlerAdapter'), ref('hibernateProxyHandler'))
                }

                boolean osivEnabled = config.getProperty('hibernate.osiv.enabled', Boolean, true)
                boolean isWebApplication = beanDefinitionRegistry?.containsBeanDefinition('dispatcherServlet') ||
                        beanDefinitionRegistry?.containsBeanDefinition('grailsControllerHelper')

                if (isWebApplication && osivEnabled && ClassUtils.isPresent('org.grails.plugin.hibernate.support.GrailsOpenSessionInViewInterceptor')) {
                    ClassLoader cl = ClassUtils.getClassLoader()
                    openSessionInViewInterceptor(cl.loadClass('org.grails.plugin.hibernate.support.GrailsOpenSessionInViewInterceptor')) {
                        hibernateDatastore = ref('hibernateDatastore')
                    }
                }
            }
        }
        return beanDefinitions
    }

    protected GenericApplicationContext createApplicationContext() {
        GenericApplicationContext applicationContext = new GenericApplicationContext()
        if (configuration instanceof ConfigurableEnvironment) {
            applicationContext.environment = (ConfigurableEnvironment) configuration
        }
        applicationContext
    }

}
