/*
 * Copyright 2016-2017 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
 *
 *      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.springframework.init.func;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.Aware;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.context.event.ApplicationContextInitializedEvent;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.OrderComparator;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

/**
 * @author Dave Syer
 *
 */
public class FunctionalInstallerListener implements SmartApplicationListener {

	private static final Log logger = LogFactory.getLog(FunctionalInstallerListener.class);

	// TODO: make this class stateless
	private Collection<ApplicationContextInitializer<GenericApplicationContext>> initializers = new LinkedHashSet<>();

	private Set<Class<? extends ApplicationContextInitializer<?>>> added = new LinkedHashSet<>();

	@Override
	public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
		return ApplicationContextInitializedEvent.class.isAssignableFrom(eventType)
				|| ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType);
	}

	@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationContextInitializedEvent) {
			ApplicationContextInitializedEvent initialized = (ApplicationContextInitializedEvent) event;
			ConfigurableApplicationContext context = initialized.getApplicationContext();
			if (!(context instanceof GenericApplicationContext)) {
				throw new IllegalStateException("ApplicationContext must be a GenericApplicationContext");
			}
			if (!isEnabled(context.getEnvironment())) {
				return;
			}
			GenericApplicationContext generic = (GenericApplicationContext) context;
			ConditionService conditions = initialize(generic);
			functional(generic, conditions);
			apply(generic, initialized.getSpringApplication(), conditions);
		}
		else if (event instanceof ApplicationEnvironmentPreparedEvent) {
			ApplicationEnvironmentPreparedEvent prepared = (ApplicationEnvironmentPreparedEvent) event;
			if (!isEnabled(prepared.getEnvironment())) {
				return;
			}
			logger.info("Preparing application context");
			SpringApplication application = prepared.getSpringApplication();
			findInitializers(application);
			WebApplicationType type = getWebApplicationType(application, prepared.getEnvironment());
			Class<?> contextType = getApplicationContextType(application);
			if (type == WebApplicationType.NONE) {
				if (contextType == AnnotationConfigApplicationContext.class || contextType == null) {
					application.setApplicationContextClass(GenericApplicationContext.class);
				}
			}
			else if (type == WebApplicationType.REACTIVE) {
				if (contextType == AnnotationConfigReactiveWebApplicationContext.class || contextType == null) {
					application.setApplicationContextClass(ReactiveWebServerApplicationContext.class);
				}
			}
			else if (type == WebApplicationType.SERVLET) {
				if (contextType == AnnotationConfigServletWebServerApplicationContext.class || contextType == null) {
					application.setApplicationContextClass(ServletWebServerApplicationContext.class);
				}
			}
		}
	}

	private WebApplicationType getWebApplicationType(SpringApplication application,
			ConfigurableEnvironment environment) {
		if (environment.getProperty("spring.main.web-application-type") != null) {
			// Environment hasn't been bound to SpringApplication yet so if this is set we
			// won't know it
			return WebApplicationType
					.valueOf(environment.getProperty("spring.main.web-application-type").toUpperCase());
		}
		return application.getWebApplicationType();
	}

	private void findInitializers(SpringApplication application) {
		for (Object source : application.getAllSources()) {
			if (source instanceof Class<?>) {
				Class<?> type = (Class<?>) source;
				String cls = type.getName().replace("$", "_") + "Initializer";
				if (ClassUtils.isPresent(cls, application.getClassLoader())) {
					@SuppressWarnings("unchecked")
					Class<? extends ApplicationContextInitializer<?>> initializer = (Class<? extends ApplicationContextInitializer<?>>) ClassUtils
							.resolveClassName(cls, application.getClassLoader());
					addInitializer(initializer);
					remove(application, source);
				}
			}
		}
		if (application.getAllSources().isEmpty()) {
			// Spring Boot is fussy and doesn't like to run with no sources
			application.addPrimarySources(Arrays.asList(Object.class));
		}
	}

	private void remove(SpringApplication application, Object source) {
		Field field = ReflectionUtils.findField(SpringApplication.class, "primarySources");
		ReflectionUtils.makeAccessible(field);
		@SuppressWarnings("unchecked")
		Set<Object> sources = (Set<Object>) ReflectionUtils.getField(field, application);
		sources.remove(source);
		application.getSources().remove(source);
	}

	private Class<?> getApplicationContextType(SpringApplication application) {
		Field field = ReflectionUtils.findField(SpringApplication.class, "applicationContextClass");
		ReflectionUtils.makeAccessible(field);
		try {
			return (Class<?>) ReflectionUtils.getField(field, application);
		}
		catch (Exception e) {
			return null;
		}
	}

	private boolean isEnabled(ConfigurableEnvironment environment) {
		return environment.getProperty("spring.functional.enabled", Boolean.class, true);
	}

	private void functional(GenericApplicationContext context, ConditionService conditions) {
		// TODO: it would be better not to have to do this
		context.registerBean(AnnotationConfigUtils.CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME,
				SlimConfigurationClassPostProcessor.class, () -> new SlimConfigurationClassPostProcessor());
		AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
	}

	private ConditionService initialize(GenericApplicationContext context) {
		if (!context.getBeanFactory().containsSingleton(ConditionService.class.getName())) {
			if (!context.getBeanFactory().containsSingleton(MetadataReaderFactory.class.getName())) {
				context.getBeanFactory().registerSingleton(MetadataReaderFactory.class.getName(),
						new CachingMetadataReaderFactory(context.getClassLoader()));
			}
			context.getBeanFactory().registerSingleton(ConditionService.class.getName(),
					new SimpleConditionService(context, context.getBeanFactory(), context.getEnvironment(), context));
			context.registerBean(ImportRegistrars.class, () -> new FunctionalInstallerImportRegistrars(context));
		}
		return (ConditionService) context.getBeanFactory().getSingleton(ConditionService.class.getName());
	}

	private void apply(GenericApplicationContext context) {
		List<ApplicationContextInitializer<GenericApplicationContext>> initializers = new ArrayList<>();
		for (ApplicationContextInitializer<GenericApplicationContext> result : this.initializers) {
			initializers.add(result);
		}
		OrderComparator.sort(initializers);
		if (logger.isDebugEnabled()) {
			logger.debug("Applying initializers: " + initializers);
		}
		for (ApplicationContextInitializer<GenericApplicationContext> initializer : initializers) {
			initializer.initialize(context);
		}
	}

	private void apply(GenericApplicationContext context, SpringApplication application, ConditionService conditions) {
		apply(context);
	}

	@SuppressWarnings("unchecked")
	private void addInitializer(Class<? extends ApplicationContextInitializer<?>> type) {
		if (type == null || this.added.contains(type)) {
			return;
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Adding initializer: " + type);
		}
		this.added.add(type);
		initializers.add(BeanUtils.instantiateClass(type, ApplicationContextInitializer.class));
	}

	public static void invokeAwareMethods(Object target, Environment environment, ResourceLoader resourceLoader,
			BeanDefinitionRegistry registry) {

		if (target instanceof Aware) {
			if (target instanceof BeanClassLoaderAware) {
				ClassLoader classLoader = (registry instanceof ConfigurableBeanFactory
						? ((ConfigurableBeanFactory) registry).getBeanClassLoader() : resourceLoader.getClassLoader());
				if (classLoader != null) {
					((BeanClassLoaderAware) target).setBeanClassLoader(classLoader);
				}
			}
			if (target instanceof BeanFactoryAware && registry instanceof BeanFactory) {
				((BeanFactoryAware) target).setBeanFactory((BeanFactory) registry);
			}
			if (target instanceof EnvironmentAware) {
				((EnvironmentAware) target).setEnvironment(environment);
			}
			if (target instanceof ResourceLoaderAware) {
				((ResourceLoaderAware) target).setResourceLoader(resourceLoader);
			}
		}
	}

}

class SlimConfigurationClassPostProcessor
		implements BeanDefinitionRegistryPostProcessor, BeanClassLoaderAware, PriorityOrdered {

	@Override
	public int getOrder() {
		return Ordered.LOWEST_PRECEDENCE - 1;
	}

	public void setMetadataReaderFactory(MetadataReaderFactory metadataReaderFactory) {
	}

	@Override
	public void setBeanClassLoader(ClassLoader classLoader) {
	}

	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
	}

	@Override
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
	}

}