/*
 *
 *  *
 *  *  *
 *  *  *  * 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.Field;
import java.util.Arrays;

import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springdoc.core.MethodAttributes;
import org.springdoc.core.SpringDocAnnotationsUtils;

import org.springframework.core.MethodParameter;
import org.springframework.data.rest.core.mapping.MethodResourceMapping;
import org.springframework.data.rest.core.mapping.ParameterMetadata;
import org.springframework.data.rest.core.mapping.ParametersMetadata;
import org.springframework.data.rest.core.mapping.ResourceDescription;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.mapping.TypedResourceDescription;
import org.springframework.data.rest.webmvc.support.DefaultedPageable;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;

/**
 * The type Data rest operation builder.
 * @author bnasslahsen
 */
public class DataRestOperationBuilder {

	/**
	 * The constant LOGGER.
	 */
	private static final Logger LOGGER = LoggerFactory.getLogger(DataRestOperationBuilder.class);

	/**
	 * The constant STRING_SEPARATOR.
	 */
	private static final String STRING_SEPARATOR = "-";

	/**
	 * The Data rest request builder.
	 */
	private DataRestRequestBuilder dataRestRequestBuilder;

	/**
	 * The Tags builder.
	 */
	private DataRestTagsBuilder tagsBuilder;

	/**
	 * The Data rest response builder.
	 */
	private DataRestResponseBuilder dataRestResponseBuilder;

	/**
	 * Instantiates a new Data rest operation builder.
	 *
	 * @param dataRestRequestBuilder the data rest request builder
	 * @param tagsBuilder the tags builder
	 * @param dataRestResponseBuilder the data rest response builder
	 */
	public DataRestOperationBuilder(DataRestRequestBuilder dataRestRequestBuilder, DataRestTagsBuilder tagsBuilder,
			DataRestResponseBuilder dataRestResponseBuilder) {
		this.dataRestRequestBuilder = dataRestRequestBuilder;
		this.tagsBuilder = tagsBuilder;
		this.dataRestResponseBuilder = dataRestResponseBuilder;
	}

	/**
	 * Build operation operation.
	 *
	 * @param handlerMethod the handler method
	 * @param domainType the domain type
	 * @param openAPI the open api
	 * @param requestMethod the request method
	 * @param operationPath the operation path
	 * @param methodAttributes the method attributes
	 * @param resourceMetadata the resource metadata
	 * @param methodResourceMapping the method resource mapping
	 * @param controllerType the controller type
	 * @return the operation
	 */
	public Operation buildOperation(HandlerMethod handlerMethod, Class<?> domainType,
			OpenAPI openAPI, RequestMethod requestMethod, String operationPath, MethodAttributes methodAttributes,
			ResourceMetadata resourceMetadata, MethodResourceMapping methodResourceMapping, ControllerType controllerType) {
		Operation operation = null;
		if (ControllerType.ENTITY.equals(controllerType)) {
			operation = buildEntityOperation(handlerMethod, domainType,
					openAPI, requestMethod, operationPath, methodAttributes, resourceMetadata);
		}
		else if (ControllerType.SEARCH.equals(controllerType)) {
			operation = buildSearchOperation(handlerMethod, domainType, openAPI, requestMethod,
					methodAttributes, methodResourceMapping);
		}
		return operation;
	}

	/**
	 * Build entity operation operation.
	 *
	 * @param handlerMethod the handler method
	 * @param domainType the domain type
	 * @param openAPI the open api
	 * @param requestMethod the request method
	 * @param operationPath the operation path
	 * @param methodAttributes the method attributes
	 * @param resourceMetadata the resource metadata
	 * @return the operation
	 */
	private Operation buildEntityOperation(HandlerMethod handlerMethod, Class<?> domainType,
			OpenAPI openAPI, RequestMethod requestMethod, String operationPath, MethodAttributes methodAttributes,
			ResourceMetadata resourceMetadata) {
		Operation operation = initOperation(handlerMethod, domainType, requestMethod);
		dataRestRequestBuilder.buildParameters(domainType, openAPI, handlerMethod, requestMethod, methodAttributes, operation, resourceMetadata);
		dataRestResponseBuilder.buildEntityResponse(operation, handlerMethod, openAPI, requestMethod, operationPath, domainType, methodAttributes);
		tagsBuilder.buildEntityTags(operation, openAPI, handlerMethod, domainType);
		if (domainType != null)
			addOperationDescription(operation, requestMethod, domainType.getSimpleName().toLowerCase());
		return operation;
	}

	/**
	 * Build search operation operation.
	 *
	 * @param handlerMethod the handler method
	 * @param domainType the domain type
	 * @param openAPI the open api
	 * @param requestMethod the request method
	 * @param methodAttributes the method attributes
	 * @param methodResourceMapping the method resource mapping
	 * @return the operation
	 */
	private Operation buildSearchOperation(HandlerMethod handlerMethod, Class<?> domainType,
			OpenAPI openAPI, RequestMethod requestMethod, MethodAttributes methodAttributes,
			MethodResourceMapping methodResourceMapping) {
		Operation operation = initOperation(handlerMethod, domainType, requestMethod);
		// Make schema as string if empty
		ParametersMetadata parameterMetadata = methodResourceMapping.getParametersMetadata();
		for (ParameterMetadata parameterMetadatum : parameterMetadata) {
			String pName = parameterMetadatum.getName();
			ResourceDescription description = parameterMetadatum.getDescription();
			if (description instanceof TypedResourceDescription) {
				TypedResourceDescription typedResourceDescription = (TypedResourceDescription) description;
				Field fieldType = FieldUtils.getField(TypedResourceDescription.class, "type", true);
				Class<?> type;
				try {
					type = (Class<?>) fieldType.get(typedResourceDescription);
				}
				catch (IllegalAccessException e) {
					LOGGER.warn(e.getMessage());
					type = String.class;
				}
				Schema<?> schema = SpringDocAnnotationsUtils.resolveSchemaFromType(type, openAPI.getComponents(), null, null);
				Parameter parameter = new Parameter().name(pName).in(ParameterIn.QUERY.toString()).schema(schema);
				operation.addParametersItem(parameter);
			}
		}
		if (methodResourceMapping.isPagingResource()) {
			MethodParameter[] parameters = handlerMethod.getMethodParameters();
			MethodParameter methodParameterPage = Arrays.stream(parameters).filter(methodParameter -> DefaultedPageable.class.equals(methodParameter.getParameterType())).findAny().orElse(null);
			if (methodParameterPage != null) {
				dataRestRequestBuilder.buildCommonParameters(domainType, openAPI, requestMethod, methodAttributes, operation, new String[] { methodParameterPage.getParameterName() }, new MethodParameter[] { methodParameterPage });
			}
		}
		dataRestResponseBuilder.buildSearchResponse(operation, handlerMethod, openAPI, methodResourceMapping, domainType, methodAttributes);
		tagsBuilder.buildSearchTags(operation, openAPI, handlerMethod, domainType);
		return operation;
	}

	/**
	 * Init operation operation.
	 *
	 * @param handlerMethod the handler method
	 * @param domainType the domain type
	 * @param requestMethod the request method
	 * @return the operation
	 */
	private Operation initOperation(HandlerMethod handlerMethod, Class<?> domainType, RequestMethod requestMethod) {
		Operation operation = new Operation();
		StringBuilder operationIdBuilder = new StringBuilder();
		operationIdBuilder.append(handlerMethod.getMethod().getName());
		if (domainType != null) {
			operationIdBuilder.append(STRING_SEPARATOR).append(domainType.getSimpleName().toLowerCase())
					.append(STRING_SEPARATOR).append(requestMethod.toString().toLowerCase());
		}
		operation.setOperationId(operationIdBuilder.toString());
		return operation;
	}

	/**
	 * Add operation description.
	 *
	 * @param operation the operation
	 * @param requestMethod the request method
	 * @param entity the entity
	 */
	private void addOperationDescription(Operation operation, RequestMethod requestMethod, String entity) {
		switch (requestMethod) {
			case GET:
				operation.setDescription("get-" + entity);
				break;
			case POST:
				operation.setDescription("create-" + entity);
				break;
			case DELETE:
				operation.setDescription("delete-" + entity);
				break;
			case PUT:
				operation.setDescription("update-" + entity);
				break;
			case PATCH:
				operation.setDescription("patch-" + entity);
				break;
			default:
				throw new IllegalArgumentException(requestMethod.name());
		}
	}
}