/*
 *
 *  Copyright 2019 uhfun
 *
 *  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 com.github.uhfun.swagger.extension;

import com.alibaba.dubbo.config.spring.ServiceBean;
import com.fasterxml.classmate.TypeResolver;
import com.github.uhfun.swagger.annotations.ApiMethod;
import com.github.uhfun.swagger.common.SwaggerMoreException;
import com.github.uhfun.swagger.util.ClassUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiParam;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.method.HandlerMethod;
import springfox.documentation.RequestHandler;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.service.RequestHandlerProvider;
import springfox.documentation.spring.web.readers.operation.HandlerMethodResolver;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.github.uhfun.swagger.common.Constant.*;
import static com.github.uhfun.swagger.util.TypeUtils.isComplexObjectType;
import static com.google.common.collect.Lists.newArrayList;
import static java.util.Collections.singletonList;
import static java.util.Objects.isNull;
import static java.util.stream.Collectors.joining;
import static springfox.documentation.builders.BuilderDefaults.nullToEmptyList;
import static springfox.documentation.spi.service.contexts.Orderings.byPatternsCondition;

/**
 * @author uhfun
 */
@Component
@ApiRequestHandlerProvider.Body
public class ApiRequestHandlerProvider implements RequestHandlerProvider {

    private final List<ServiceBean> serviceBeans;
    private final HandlerMethodResolver methodResolver;
    private final TypeResolver typeResolver;

    @Autowired
    public ApiRequestHandlerProvider(List<ServiceBean> serviceBeans,
                                     HandlerMethodResolver methodResolver,
                                     TypeResolver typeResolver) {
        this.serviceBeans = serviceBeans;
        this.methodResolver = methodResolver;
        this.typeResolver = typeResolver;
    }

    @Override
    public List<RequestHandler> requestHandlers() {
        return byPatternsCondition().sortedCopy(nullToEmptyList(serviceBeans).stream()
                .filter(bean -> AnnotatedElementUtils.hasAnnotation(bean.getInterfaceClass(), Api.class))
                .reduce(newArrayList(), toMappingEntries(), (o1, o2) -> o1)
                .stream().map(toRequestHandler()).collect(Collectors.toList()));
    }

    private BiFunction<List<HandlerMethod>, ? super ServiceBean,
            List<HandlerMethod>> toMappingEntries() {
        return (list, bean) -> {
            Object object = AopUtils.isAopProxy(bean.getRef())
                    ? AopProxyUtils.getSingletonTarget(bean.getRef()) : bean.getRef();
            list.addAll(Arrays.stream(bean.getInterfaceClass().getDeclaredMethods())
                    .filter(method -> !Modifier.isStatic(method.getModifiers()))
                    .filter(method -> AnnotatedElementUtils.hasAnnotation(method, ApiMethod.class))
                    .map(method -> new HandlerMethod(object, method))
                    .collect(Collectors.toList()));
            return list;
        };
    }

    private Function<HandlerMethod, RequestHandler> toRequestHandler() {
        return handlerMethod -> new ApiRequestHandler(methodResolver, handlerMethod,
                allAsRequestBody(handlerMethod)
                        ? annotateBody(handlerMethod)
                        : methodResolver.methodParameters(handlerMethod));
    }

    private List<ResolvedMethodParameter> annotateBody(HandlerMethod handlerMethod) {
        ResolvedMethodParameter param0;
        List<Class> parameters = newArrayList(handlerMethod.getMethod().getParameterTypes());
        if (parameters.size() == 1) {
            param0 = methodResolver.methodParameters(handlerMethod).get(0);
        } else {
            Class<?> generatedType = mergeIntoGeneratedType(parameters, handlerMethod.getMethod());
            param0 = new ResolvedMethodParameter(generatedType.getSimpleName(),
                    new MethodParameter(handlerMethod.getMethod(), 0),
                    typeResolver.resolve(generatedType));
        }
        return singletonList(param0.annotate(AnnotationUtils.findAnnotation(getClass(), Body.class).body()));
    }

    private Class<?> mergeIntoGeneratedType(List<Class> parameters, Method method) {
        ApiMethod apiMethod = AnnotationUtils.findAnnotation(method, ApiMethod.class);
        if (isNull(apiMethod)) {
            throw new SwaggerMoreException("Method " + method.getDeclaringClass().getName() + "." + method.getName() + "has more than two complex parameters that must be annotated @ApiMethod with @ApiParam");
        }
        String className = DEFAULT_PACKAGE_NAME +
                method.getDeclaringClass().getSimpleName() + DOT +
                GENERATED_PREFIX + method.getName() + UNDERLINE +
                parameters.stream().map(Class::getSimpleName).collect(joining("_")) +
                DEFAULT_COMPLEX_OBJECT_SUFFIX;
        List<String> names = newArrayList();
        List<String> values = newArrayList();
        for (ApiParam param : apiMethod.params()) {
            names.add(param.name());
            values.add(param.value());
        }
        return ClassUtils.make(className, method.getParameterTypes(), names, values);
    }

    private boolean allAsRequestBody(HandlerMethod handlerMethod) {
        return Stream.of(handlerMethod.getMethod().getParameters()).anyMatch(p -> isComplexObjectType(p.getType()));
    }

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @interface Body {
        RequestBody body() default @RequestBody;
    }
}