/*
 * Copyright 2002-2019 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.springframework.messaging.handler.invocation.reactive;

import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;

import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.messaging.handler.MessagingAdviceBean;
import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver;
import org.springframework.util.Assert;

/**
 * Help to initialize and invoke an {@link InvocableHandlerMethod}, and to then
 * apply return value handling and exception handling. Holds all necessary
 * configuration necessary to do so.
 *
 * @author Rossen Stoyanchev
 * @since 5.2
 */
class InvocableHelper {

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


	private final HandlerMethodArgumentResolverComposite argumentResolvers =
			new HandlerMethodArgumentResolverComposite();

	private final HandlerMethodReturnValueHandlerComposite returnValueHandlers =
			new HandlerMethodReturnValueHandlerComposite();

	private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();

	private final Function<Class<?>, AbstractExceptionHandlerMethodResolver> exceptionMethodResolverFactory;

	private final Map<Class<?>, AbstractExceptionHandlerMethodResolver> exceptionHandlerCache =
			new ConcurrentHashMap<>(64);

	private final Map<MessagingAdviceBean, AbstractExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
			new LinkedHashMap<>(64);


	public InvocableHelper(
			Function<Class<?>, AbstractExceptionHandlerMethodResolver> exceptionMethodResolverFactory) {

		this.exceptionMethodResolverFactory = exceptionMethodResolverFactory;
	}

	/**
	 * Add the arguments resolvers to use for message handling and exception
	 * handling methods.
	 */
	public void addArgumentResolvers(List<? extends HandlerMethodArgumentResolver> resolvers) {
		this.argumentResolvers.addResolvers(resolvers);
	}

	/**
	 * Add the return value handlers to use for message handling and exception
	 * handling methods.
	 */
	public void addReturnValueHandlers(List<? extends HandlerMethodReturnValueHandler> handlers) {
		this.returnValueHandlers.addHandlers(handlers);
	}

	/**
	 * Configure the registry for adapting various reactive types.
	 * <p>By default this is an instance of {@link ReactiveAdapterRegistry} with
	 * default settings.
	 */
	public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
		Assert.notNull(registry, "ReactiveAdapterRegistry is required");
		this.reactiveAdapterRegistry = registry;
	}

	/**
	 * Return the configured registry for adapting reactive types.
	 */
	public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
		return this.reactiveAdapterRegistry;
	}

	/**
	 * Method to populate the MessagingAdviceBean cache (e.g. to support "global"
	 * {@code @MessageExceptionHandler}).
	 */
	public void registerExceptionHandlerAdvice(
			MessagingAdviceBean bean, AbstractExceptionHandlerMethodResolver resolver) {

		this.exceptionHandlerAdviceCache.put(bean, resolver);
	}


	/**
	 * Create {@link InvocableHandlerMethod} with the configured arg resolvers.
	 * @param handlerMethod the target handler method to invoke
	 * @return the created instance
	 */

	public InvocableHandlerMethod initMessageMappingMethod(HandlerMethod handlerMethod) {
		InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod);
		invocable.setArgumentResolvers(this.argumentResolvers.getResolvers());
		return invocable;
	}

	/**
	 * Find an exception handling method for the given exception.
	 * <p>The default implementation searches methods in the class hierarchy of
	 * the HandlerMethod first and if not found, it continues searching for
	 * additional handling methods registered via
	 * {@link #registerExceptionHandlerAdvice}.
	 * @param handlerMethod the method where the exception was raised
	 * @param ex the exception raised or signaled
	 * @return a method to handle the exception, or {@code null}
	 */
	@Nullable
	public InvocableHandlerMethod initExceptionHandlerMethod(HandlerMethod handlerMethod, Throwable ex) {
		if (logger.isDebugEnabled()) {
			logger.debug("Searching for methods to handle " + ex.getClass().getSimpleName());
		}
		Class<?> beanType = handlerMethod.getBeanType();
		AbstractExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(beanType);
		if (resolver == null) {
			resolver = this.exceptionMethodResolverFactory.apply(beanType);
			this.exceptionHandlerCache.put(beanType, resolver);
		}
		InvocableHandlerMethod exceptionHandlerMethod = null;
		Method method = resolver.resolveMethod(ex);
		if (method != null) {
			exceptionHandlerMethod = new InvocableHandlerMethod(handlerMethod.getBean(), method);
		}
		else {
			for (MessagingAdviceBean advice : this.exceptionHandlerAdviceCache.keySet()) {
				if (advice.isApplicableToBeanType(beanType)) {
					resolver = this.exceptionHandlerAdviceCache.get(advice);
					method = resolver.resolveMethod(ex);
					if (method != null) {
						exceptionHandlerMethod = new InvocableHandlerMethod(advice.resolveBean(), method);
						break;
					}
				}
			}
		}
		if (exceptionHandlerMethod != null) {
			logger.debug("Found exception handler " + exceptionHandlerMethod.getShortLogMessage());
			exceptionHandlerMethod.setArgumentResolvers(this.argumentResolvers.getResolvers());
		}
		else {
			logger.error("No exception handling method", ex);
		}
		return exceptionHandlerMethod;
	}


	public Mono<Void> handleMessage(HandlerMethod handlerMethod, Message<?> message) {
		InvocableHandlerMethod invocable = initMessageMappingMethod(handlerMethod);
		if (logger.isDebugEnabled()) {
			logger.debug("Invoking " + invocable.getShortLogMessage());
		}
		return invocable.invoke(message)
				.switchIfEmpty(Mono.defer(() -> handleReturnValue(null, invocable, message)))
				.flatMap(returnValue -> handleReturnValue(returnValue, invocable, message))
				.onErrorResume(ex -> {
					InvocableHandlerMethod exHandler = initExceptionHandlerMethod(handlerMethod, ex);
					if (exHandler == null) {
						return Mono.error(ex);
					}
					if (logger.isDebugEnabled()) {
						logger.debug("Invoking " + exHandler.getShortLogMessage());
					}
					return exHandler.invoke(message, ex)
							.switchIfEmpty(Mono.defer(() -> handleReturnValue(null, exHandler, message)))
							.flatMap(returnValue -> handleReturnValue(returnValue, exHandler, message));
				});
	}

	private Mono<Void> handleReturnValue(
			@Nullable Object returnValue, HandlerMethod handlerMethod, Message<?> message) {

		MethodParameter returnType = handlerMethod.getReturnType();
		return this.returnValueHandlers.handleReturnValue(returnValue, returnType, message);
	}

}
