/* 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 org.grails.datastore.gorm.mongodb.boot.autoconfigure

import java.beans.Introspector

import groovy.transform.CompileStatic

import com.mongodb.MongoClientSettings
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients

import org.springframework.beans.BeansException
import org.springframework.beans.factory.DisposableBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
import org.springframework.boot.autoconfigure.AutoConfigurationPackages
import org.springframework.boot.autoconfigure.AutoConfigureAfter
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.mongodb.autoconfigure.MongoAutoConfiguration
import org.springframework.boot.mongodb.autoconfigure.MongoProperties
import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware
import org.springframework.context.ConfigurableApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.env.ConfigurableEnvironment
import org.springframework.transaction.PlatformTransactionManager

import org.grails.datastore.gorm.events.ConfigurableApplicationContextEventPublisher
import org.grails.datastore.mapping.mongo.MongoDatastore
import org.grails.datastore.mapping.services.Service

/**
 *
 * Auto configurer that configures GORM for MongoDB for use in Spring Boot
 *
 * @author Graeme Rocher
 * @since 1.0
 */
@CompileStatic
@Configuration
@ConditionalOnMissingBean(MongoDatastore)
@AutoConfigureAfter(MongoAutoConfiguration)
class MongoDbGormAutoConfiguration implements ApplicationContextAware, DisposableBean {

    @Autowired(required = false)
    private MongoProperties mongoProperties

    @Autowired(required = false)
    MongoClient mongo

    @Autowired(required = false)
    MongoClientSettings mongoOptions

    ConfigurableApplicationContext applicationContext

    /**
     * Whether the {@link #mongo} client was created by this auto-configuration rather than supplied
     * as an existing bean. Only an internally created client is closed on shutdown; a client provided
     * as a bean is owned by the application context (or the user), and GORM no longer closes a client
     * it did not create.
     */
    private boolean mongoClientCreatedInternally = false

    @Bean
    MongoDatastore mongoDatastore() {
        ConfigurableApplicationContext context = applicationContext
        if (!(context instanceof ConfigurableApplicationContext)) {
            throw new IllegalArgumentException('MongoDbGormAutoConfiguration requires an instance of ConfigurableApplicationContext')
        }
        ConfigurableListableBeanFactory beanFactory = context.beanFactory
        List<String> packageNames = AutoConfigurationPackages.get(beanFactory)
        List<Package> packages = []
        for (name in packageNames) {
            Package pkg = Package.getPackage(name)
            if (pkg != null) {
                packages.add(pkg)
            }
        }

        MongoDatastore datastore
        ConfigurableEnvironment environment = context.environment
        ConfigurableApplicationContextEventPublisher eventPublisher = new ConfigurableApplicationContextEventPublisher(context)
        if (mongo != null) {
            datastore = new MongoDatastore(mongo, environment, eventPublisher, packages as Package[])
        }
        else if (mongoProperties != null) {
            this.mongo = MongoClients.create(mongoOptions)
            this.mongoClientCreatedInternally = true
            datastore = new MongoDatastore(mongo, environment, eventPublisher, packages as Package[])
        }
        else {
            datastore = new MongoDatastore(environment, eventPublisher, packages as Package[])
        }

        for (Service service in datastore.getServices()) {
            Class serviceClass = service.getClass()
            grails.gorm.services.Service ann = serviceClass.getAnnotation(grails.gorm.services.Service)
            String serviceName = ann?.name()
            if (serviceName == null) {
                serviceName = Introspector.decapitalize(serviceClass.simpleName)
            }
            if (!context.containsBean(serviceName)) {
                context.beanFactory.registerSingleton(
                        serviceName,
                        service
                )
            }
        }
        return datastore
    }

    @Bean
    PlatformTransactionManager mongoTransactionManager() {
        mongoDatastore().getTransactionManager()
    }

    @Override
    void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (!(applicationContext instanceof ConfigurableApplicationContext)) {
            throw new IllegalArgumentException('MongoDbGormAutoConfiguration requires an instance of ConfigurableApplicationContext')
        }
        this.applicationContext = (ConfigurableApplicationContext) applicationContext
    }

    /**
     * Closes the {@link MongoClient} only when it was created by this auto-configuration. A client
     * supplied as an existing bean is owned by the application context (or the user) and must not be
     * closed here. The {@code mongoDatastore} bean is created after this configuration, so it is
     * destroyed first, leaving its sessions closed before the client itself is closed.
     */
    @Override
    void destroy() throws Exception {
        if (mongoClientCreatedInternally && mongo != null) {
            mongo.close()
        }
    }

}
