/*
 * Copyright 2017-2020 original 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
 *
 * 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 io.micronaut.configuration.hibernate.jpa;

import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.context.annotation.EachProperty;
import io.micronaut.context.env.Environment;
import io.micronaut.core.beans.BeanIntrospection;
import io.micronaut.core.beans.BeanIntrospector;
import io.micronaut.core.convert.format.MapFormat;
import io.micronaut.core.naming.conventions.StringConvention;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.core.util.Toggleable;
import org.hibernate.boot.registry.BootstrapServiceRegistry;
import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.integrator.spi.Integrator;

import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.persistence.Entity;
import javax.validation.ValidatorFactory;
import java.util.*;

/**
 * Configuration for JPA and Hibernate.
 *
 * @author graemerocher
 * @since 1.0
 */
@EachProperty(value = JpaConfiguration.PREFIX, primary = "default")
public class JpaConfiguration {
    public static final String PREFIX = "jpa";

    private final BootstrapServiceRegistry bootstrapServiceRegistry;
    private final Environment environment;
    private final ApplicationContext applicationContext;
    private Map<String, Object> jpaProperties = new HashMap<>(10);
    private EntityScanConfiguration entityScanConfiguration;

    /**
     * @param applicationContext The application context
     * @param integrator         The {@link Integrator}
     */
    protected JpaConfiguration(ApplicationContext applicationContext,
                               @Nullable Integrator integrator) {
        this(applicationContext, integrator, new EntityScanConfiguration(applicationContext.getEnvironment()));
    }

    /**
     * @param applicationContext      The application context
     * @param integrator              The {@link Integrator}
     * @param entityScanConfiguration The entity scan configuration
     */
    @Inject
    protected JpaConfiguration(ApplicationContext applicationContext,
                               @Nullable Integrator integrator,
                               @Nullable EntityScanConfiguration entityScanConfiguration) {
        ClassLoader classLoader = applicationContext.getClassLoader();
        BootstrapServiceRegistryBuilder bootstrapServiceRegistryBuilder =
                createBootstrapServiceRegistryBuilder(integrator, classLoader);

        this.bootstrapServiceRegistry = bootstrapServiceRegistryBuilder.build();
        this.entityScanConfiguration = entityScanConfiguration != null ? entityScanConfiguration : new EntityScanConfiguration(applicationContext.getEnvironment());
        this.environment = applicationContext.getEnvironment();
        this.applicationContext = applicationContext;
    }

    /**
     * @return The entity scan configuration
     */
    public EntityScanConfiguration getEntityScanConfiguration() {
        return entityScanConfiguration;
    }

    /**
     * Builds the standard service registry.
     *
     * @param additionalSettings Additional settings for the service registry
     * @return The standard service registry
     */
    @SuppressWarnings("WeakerAccess")
    public StandardServiceRegistry buildStandardServiceRegistry(@Nullable Map<String, Object> additionalSettings) {
        StandardServiceRegistryBuilder standardServiceRegistryBuilder = createStandServiceRegistryBuilder(bootstrapServiceRegistry);

        Map<String, Object> jpaProperties = getProperties();
        if (CollectionUtils.isNotEmpty(jpaProperties)) {
            standardServiceRegistryBuilder.applySettings(jpaProperties);
        }
        if (additionalSettings != null) {
            standardServiceRegistryBuilder.applySettings(additionalSettings);
        }
        return standardServiceRegistryBuilder.build();
    }

    /**
     * Sets the packages to scan.
     *
     * @param packagesToScan The packages to scan
     */
    public void setPackagesToScan(String... packagesToScan) {
        if (ArrayUtils.isNotEmpty(packagesToScan)) {
            EntityScanConfiguration entityScanConfiguration = new EntityScanConfiguration(environment);
            entityScanConfiguration.setClasspath(true);
            entityScanConfiguration.setPackages(packagesToScan);
            this.entityScanConfiguration = entityScanConfiguration;
        }
    }

    /**
     * @return The packages to scan
     */
    public String[] getPackagesToScan() {
        return entityScanConfiguration.getPackages();
    }

    /**
     * Sets the JPA properties to be passed to the JPA implementation.
     *
     * @param jpaProperties The JPA properties
     */
    public final void setProperties(
            @MapFormat(transformation = MapFormat.MapTransformation.FLAT, keyFormat = StringConvention.RAW)
                    Map<String, Object> jpaProperties) {
        this.jpaProperties = jpaProperties;
    }

    /**
     * @return The JPA properties
     */
    public Map<String, Object> getProperties() {
        ValidatorFactory validatorFactory;
        if (applicationContext.containsBean(ValidatorFactory.class)) {
            validatorFactory = applicationContext.getBean(ValidatorFactory.class);
        } else {
            validatorFactory = null;
        }

        if (validatorFactory != null) {
            jpaProperties.put(org.hibernate.cfg.AvailableSettings.JPA_VALIDATION_FACTORY, validatorFactory);
        }
        return jpaProperties;
    }

    /**
     * Creates the default {@link BootstrapServiceRegistryBuilder}.
     *
     * @param integrator  The integrator to use. Can be null
     * @param classLoader The class loade rto use
     * @return The BootstrapServiceRegistryBuilder
     */
    @SuppressWarnings("WeakerAccess")
    protected BootstrapServiceRegistryBuilder createBootstrapServiceRegistryBuilder(
            @Nullable Integrator integrator,
            ClassLoader classLoader) {
        BootstrapServiceRegistryBuilder bootstrapServiceRegistryBuilder = new BootstrapServiceRegistryBuilder();
        bootstrapServiceRegistryBuilder.applyClassLoader(classLoader);
        if (integrator != null) {
            bootstrapServiceRegistryBuilder.applyIntegrator(integrator);
        }
        return bootstrapServiceRegistryBuilder;
    }

    /**
     * Creates the standard service registry builder.
     *
     * @param bootstrapServiceRegistry The {@link BootstrapServiceRegistry} instance
     * @return The {@link StandardServiceRegistryBuilder} instance
     */
    @SuppressWarnings("WeakerAccess")
    protected StandardServiceRegistryBuilder createStandServiceRegistryBuilder(BootstrapServiceRegistry bootstrapServiceRegistry) {
        return new StandardServiceRegistryBuilder(
                bootstrapServiceRegistry
        );
    }

    /**
     * The entity scan configuration.
     */
    @ConfigurationProperties("entity-scan")
    public static class EntityScanConfiguration implements Toggleable {

        private boolean enabled = true;
        private boolean classpath = false;
        private String[] packages = StringUtils.EMPTY_STRING_ARRAY;

        private final Environment environment;

        /**
         * Default constructor.
         *
         * @param environment The environment
         */
        public EntityScanConfiguration(Environment environment) {
            this.environment = environment;
        }

        @Override
        public boolean isEnabled() {
            return enabled;
        }

        /**
         * @return Whether to scan the whole classpath or just look for introspected beans compiled by this application.
         */
        public boolean isClasspath() {
            return classpath;
        }

        /**
         * Sets whether to scan the whole classpath including external JAR files using classpath scanning or just look for introspected beans compiled by this application.
         *
         * @param classpath True if extensive classpath scanning should be used
         */
        public void setClasspath(boolean classpath) {
            this.classpath = classpath;
        }

        /**
         * Set whether entity scan is enabled. Defaults to true.
         *
         * @param enabled True if it is enabled
         */
        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }

        /**
         * The packages to limit the scan to.
         *
         * @return The packages to limit the scan to
         */
        public String[] getPackages() {
            return packages;
        }

        /**
         * @param packages The packages
         */
        public void setPackages(String[] packages) {
            this.packages = packages;
        }

        /**
         * Find entities for the current configuration.
         *
         * @return The entities
         */
        public Collection<Class<?>> findEntities() {
            Collection<Class<?>> entities = new HashSet<>();
            if (isClasspath()) {

                if (ArrayUtils.isNotEmpty(packages)) {
                    environment.scan(Entity.class, packages).forEach(entities::add);
                } else {
                    environment.scan(Entity.class).forEach(entities::add);
                }
            }

            if (isEnabled()) {
                Collection<BeanIntrospection<Object>> introspections;
                if (ArrayUtils.isNotEmpty(packages)) {
                    introspections = BeanIntrospector.SHARED.findIntrospections(Entity.class, packages);
                } else {
                    introspections = BeanIntrospector.SHARED.findIntrospections(Entity.class);
                }
                introspections
                        .stream().map(BeanIntrospection::getBeanType)
                        .forEach(entities::add);
            }
            return Collections.unmodifiableCollection(entities);
        }
    }
}