/*
 * Copyright 2002-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
 *
 *      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.web.reactive.result.method.annotation;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import reactor.core.publisher.Mono;

import org.springframework.core.Conventions;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.result.method.InvocableHandlerMethod;
import org.springframework.web.server.ServerWebExchange;


/**
 * Package-private class to assist {@link RequestMappingHandlerAdapter} with
 * default model initialization through {@code @ModelAttribute} methods.
 *
 * @author Rossen Stoyanchev
 * @since 5.0
 */
class ModelInitializer {

	private final ReactiveAdapterRegistry adapterRegistry;


	public ModelInitializer(ReactiveAdapterRegistry adapterRegistry) {
		this.adapterRegistry = adapterRegistry;
	}


	private ReactiveAdapterRegistry getAdapterRegistry() {
		return this.adapterRegistry;
	}


	/**
	 * Initialize the default model in the given {@code BindingContext} through
	 * the {@code @ModelAttribute} methods and indicate when complete.
	 *
	 * <p>This will wait for {@code @ModelAttribute} methods that return
	 * {@code Mono<Void>} since those may be adding attributes asynchronously.
	 * However if methods return async attributes, those will be added to the
	 * model as-is and without waiting for them to be resolved.
	 *
	 * @param bindingContext the BindingContext with the default model
	 * @param attributeMethods the {@code @ModelAttribute} methods
	 * @param exchange the current exchange
	 *
	 * @return a {@code Mono} for when the model is populated.
	 */
	@SuppressWarnings("Convert2MethodRef")
	public Mono<Void> initModel(BindingContext bindingContext,
			List<InvocableHandlerMethod> attributeMethods, ServerWebExchange exchange) {

		List<Mono<HandlerResult>> resultList = new ArrayList<>();
		attributeMethods.forEach(invocable -> resultList.add(invocable.invoke(exchange, bindingContext)));

		return Mono.when(resultList, objectArray -> {
			return Arrays.stream(objectArray)
					.map(object -> (HandlerResult) object)
					.map(handlerResult -> handleResult(handlerResult, bindingContext))
					.collect(Collectors.toList());
		}).flatMap(completionList -> Mono.when(completionList));
	}

	private Mono<Void> handleResult(HandlerResult handlerResult, BindingContext bindingContext) {

		return handlerResult.getReturnValue()
				.map(value -> {
					ResolvableType type = handlerResult.getReturnType();
					ReactiveAdapter adapter = getAdapterRegistry().getAdapter(type.getRawClass(), value);

					Class<?> attributeType;
					if (adapter != null) {
						attributeType = adapter.isNoValue() ? Void.class : type.resolveGeneric(0);
						if (attributeType.equals(Void.class)) {
							return Mono.<Void>from(adapter.toPublisher(value));
						}
					}
					else {
						attributeType = type.resolve();
					}

					String name = getAttributeName(handlerResult.getReturnTypeSource());
					bindingContext.getModel().asMap().putIfAbsent(name, value);
					return Mono.<Void>empty();
				})
				.orElse(Mono.empty());
	}

	private String getAttributeName(MethodParameter param) {
		return Optional
				.ofNullable(AnnotatedElementUtils.findMergedAnnotation(param.getMethod(), ModelAttribute.class))
				.filter(ann -> StringUtils.hasText(ann.value()))
				.map(ModelAttribute::value)
				.orElse(Conventions.getVariableNameForParameter(param));
	}

}
