/*
 *
 *  *
 *  *  *
 *  *  *  * Copyright 2019-2020 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.springdoc.data.rest.core;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import org.springdoc.core.GenericResponseBuilder;
import org.springdoc.core.MethodAttributes;
import org.springdoc.core.ReturnTypeParser;

import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.data.rest.core.mapping.MethodResourceMapping;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.PagedModel;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;

/**
 * The type Data rest response builder.
 * @author bnasslahsen
 */
public class DataRestResponseBuilder {

	/**
	 * The Generic response builder.
	 */
	private GenericResponseBuilder genericResponseBuilder;

	/**
	 * Instantiates a new Data rest response builder.
	 *
	 * @param genericResponseBuilder the generic response builder
	 */
	public DataRestResponseBuilder(GenericResponseBuilder genericResponseBuilder) {
		this.genericResponseBuilder = genericResponseBuilder;
	}

	/**
	 * Build search response.
	 *
	 * @param operation the operation
	 * @param handlerMethod the handler method
	 * @param openAPI the open api
	 * @param methodResourceMapping the method resource mapping
	 * @param domainType the domain type
	 * @param methodAttributes the method attributes
	 */
	public void buildSearchResponse(Operation operation, HandlerMethod handlerMethod, OpenAPI openAPI,
			MethodResourceMapping methodResourceMapping, Class<?> domainType, MethodAttributes methodAttributes) {
		MethodParameter methodParameterReturn = handlerMethod.getReturnType();
		ApiResponses apiResponses = new ApiResponses();
		ApiResponse apiResponse = new ApiResponse();
		Type returnType = findSearchReturnType(handlerMethod, methodResourceMapping, domainType);
		Content content = genericResponseBuilder.buildContent(openAPI.getComponents(), methodParameterReturn.getParameterAnnotations(), methodAttributes.getMethodProduces(), null, returnType);
		apiResponse.setContent(content);
		addResponse200(apiResponses, apiResponse);
		addResponse404(apiResponses);
		operation.setResponses(apiResponses);
	}


	/**
	 * Build entity response.
	 *
	 * @param operation the operation
	 * @param handlerMethod the handler method
	 * @param openAPI the open api
	 * @param requestMethod the request method
	 * @param operationPath the operation path
	 * @param domainType the domain type
	 * @param methodAttributes the method attributes
	 */
	public void buildEntityResponse(Operation operation, HandlerMethod handlerMethod, OpenAPI openAPI, RequestMethod requestMethod,
			String operationPath, Class<?> domainType, MethodAttributes methodAttributes) {
		MethodParameter methodParameterReturn = handlerMethod.getReturnType();
		Type returnType = ReturnTypeParser.resolveType(methodParameterReturn.getGenericParameterType(), methodParameterReturn.getContainingClass());
		returnType = getType(returnType, domainType);
		ApiResponses apiResponses = new ApiResponses();
		ApiResponse apiResponse = new ApiResponse();
		Content content = genericResponseBuilder.buildContent(openAPI.getComponents(), methodParameterReturn.getParameterAnnotations(), methodAttributes.getMethodProduces(), null, returnType);
		apiResponse.setContent(content);
		addResponse(requestMethod, operationPath, apiResponses, apiResponse);
		operation.setResponses(apiResponses);
	}

	/**
	 * Add response.
	 *
	 * @param requestMethod the request method
	 * @param operationPath the operation path
	 * @param apiResponses the api responses
	 * @param apiResponse the api response
	 */
	private void addResponse(RequestMethod requestMethod, String operationPath, ApiResponses apiResponses, ApiResponse apiResponse) {
		switch (requestMethod) {
			case GET:
				addResponse200(apiResponses, apiResponse);
				if (operationPath.contains("/{id}"))
					addResponse404(apiResponses);
				break;
			case POST:
				apiResponses.put(String.valueOf(HttpStatus.CREATED.value()), apiResponse.description(HttpStatus.CREATED.getReasonPhrase()));
				break;
			case DELETE:
				addResponse204(apiResponses);
				addResponse404(apiResponses);
				break;
			case PUT:
				addResponse200(apiResponses, apiResponse);
				apiResponses.put(String.valueOf(HttpStatus.CREATED.value()), new ApiResponse().content(apiResponse.getContent()).description(HttpStatus.CREATED.getReasonPhrase()));
				addResponse204(apiResponses);
				break;
			case PATCH:
				addResponse200(apiResponses, apiResponse);
				addResponse204(apiResponses);
				break;
			default:
				throw new IllegalArgumentException(requestMethod.name());
		}
	}

	/**
	 * Find search return type type.
	 *
	 * @param handlerMethod the handler method
	 * @param methodResourceMapping the method resource mapping
	 * @param domainType the domain type
	 * @return the type
	 */
	private Type findSearchReturnType(HandlerMethod handlerMethod, MethodResourceMapping methodResourceMapping, Class<?> domainType) {
		Type returnType = null;
		Type returnRepoType = ReturnTypeParser.resolveType(methodResourceMapping.getMethod().getGenericReturnType(), methodResourceMapping.getMethod().getDeclaringClass());
		if (methodResourceMapping.isPagingResource()) {
			returnType = ResolvableType.forClassWithGenerics(PagedModel.class, domainType).getType();
		}
		else if (Iterable.class.isAssignableFrom(ResolvableType.forType(returnRepoType).getRawClass())) {
			returnType = ResolvableType.forClassWithGenerics(CollectionModel.class, domainType).getType();
		}
		else if (!ClassUtils.isPrimitiveOrWrapper(domainType)) {
			returnType = ResolvableType.forClassWithGenerics(EntityModel.class, domainType).getType();
		}
		if (returnType == null) {
			returnType = ReturnTypeParser.resolveType(handlerMethod.getMethod().getGenericReturnType(), handlerMethod.getBeanType());
			returnType = getType(returnType, domainType);
		}
		return returnType;
	}

	/**
	 * Gets type.
	 *
	 * @param returnType the return type
	 * @param domainType the domain type
	 * @return the type
	 */
	private Type getType(Type returnType, Class<?> domainType) {
		if (returnType instanceof ParameterizedType) {
			ParameterizedType parameterizedType = (ParameterizedType) returnType;
			if ((ResponseEntity.class.equals(parameterizedType.getRawType()))) {
				if (Object.class.equals(parameterizedType.getActualTypeArguments()[0])) {
					return ResolvableType.forClassWithGenerics(ResponseEntity.class, domainType).getType();
				}
				else if (parameterizedType.getActualTypeArguments()[0] instanceof ParameterizedType) {
					ParameterizedType parameterizedType1 = (ParameterizedType) parameterizedType.getActualTypeArguments()[0];
					Class<?> rawType = ResolvableType.forType(parameterizedType1.getRawType()).getRawClass();
					if (rawType != null && rawType.isAssignableFrom(RepresentationModel.class)) {
						return resolveGenericType(ResponseEntity.class, RepresentationModel.class, domainType);
					}
					else if (EntityModel.class.equals(parameterizedType1.getRawType())) {
						return resolveGenericType(ResponseEntity.class, EntityModel.class, domainType);
					}
				}
				else if (parameterizedType.getActualTypeArguments()[0] instanceof WildcardType) {
					WildcardType wildcardType = (WildcardType) parameterizedType.getActualTypeArguments()[0];
					if (wildcardType.getUpperBounds()[0] instanceof ParameterizedType) {
						ParameterizedType wildcardTypeUpperBound = (ParameterizedType) wildcardType.getUpperBounds()[0];
						if (RepresentationModel.class.equals(wildcardTypeUpperBound.getRawType())) {
							return resolveGenericType(ResponseEntity.class, RepresentationModel.class, domainType);
						}
					}
				}
			}
			else if ((HttpEntity.class.equals(parameterizedType.getRawType())
					&& parameterizedType.getActualTypeArguments()[0] instanceof ParameterizedType)) {
				ParameterizedType wildcardTypeUpperBound = (ParameterizedType) parameterizedType.getActualTypeArguments()[0];
				if (RepresentationModel.class.equals(wildcardTypeUpperBound.getRawType())) {
					return resolveGenericType(HttpEntity.class, RepresentationModel.class, domainType);
				}
			}
			else if ((CollectionModel.class.equals(parameterizedType.getRawType())
					&& Object.class.equals(parameterizedType.getActualTypeArguments()[0]))) {
				return ResolvableType.forClassWithGenerics(CollectionModel.class, domainType).getType();
			}
		}
		return returnType;
	}

	/**
	 * Resolve generic type type.
	 *
	 * @param container the container
	 * @param generic the generic
	 * @param domainType the domain type
	 * @return the type
	 */
	private Type resolveGenericType(Class<?> container, Class<?> generic, Class<?> domainType) {
		Type type = ResolvableType.forClassWithGenerics(generic, domainType).getType();
		return ResolvableType.forClassWithGenerics(container, ResolvableType.forType(type)).getType();
	}

	/**
	 * Add response 200.
	 *
	 * @param apiResponses the api responses
	 * @param apiResponse the api response
	 */
	private void addResponse200(ApiResponses apiResponses, ApiResponse apiResponse) {
		apiResponses.put(String.valueOf(HttpStatus.OK.value()), apiResponse.description(HttpStatus.OK.getReasonPhrase()));
	}

	/**
	 * Add response 204.
	 *
	 * @param apiResponses the api responses
	 */
	private void addResponse204(ApiResponses apiResponses) {
		apiResponses.put(String.valueOf(HttpStatus.NO_CONTENT.value()), new ApiResponse().description(HttpStatus.NO_CONTENT.getReasonPhrase()));
	}

	/**
	 * Add response 404.
	 *
	 * @param apiResponses the api responses
	 */
	private void addResponse404(ApiResponses apiResponses) {
		apiResponses.put(String.valueOf(HttpStatus.NOT_FOUND.value()), new ApiResponse().description(HttpStatus.NOT_FOUND.getReasonPhrase()));
	}
}