package com.alibaba.spring.web.servlet.mvc.util;

import com.alibaba.spring.util.BeanUtils;
import com.alibaba.spring.web.util.WebUtils;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.i18n.LocaleContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.filter.RequestContextFilter;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.FrameworkServlet;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import static org.springframework.util.ReflectionUtils.findMethod;
import static org.springframework.util.ReflectionUtils.invokeMethod;
import static org.springframework.web.context.ContextLoader.CONTEXT_INITIALIZER_CLASSES_PARAM;
import static org.springframework.web.context.ContextLoader.GLOBAL_INITIALIZER_CLASSES_PARAM;

/**
 * Web MVC Utilities Class
 *
 * @author <a href="mailto:[email protected]">Mercy</a>
 * @version 1.0.1
 * @see ServletContext
 * @since 1.0.0 2016-10-10
 */
public abstract class WebMvcUtils {

    /**
     * The name of AbstractJsonpResponseBodyAdvice class which was present in Spring Framework since 4.1
     */
    public static final String ABSTRACT_JSONP_RESPONSE_BODY_ADVICE_CLASS_NAME =
            "org.springframework.web.servlet.mvc.findWebApplicationContextMethod.annotation.AbstractJsonpResponseBodyAdvice";

    /**
     * Indicates current version of Spring Framework is 4.1 or above
     */
    private final static boolean ABSTRACT_JSONP_RESPONSE_BODY_ADVICE_PRESENT =
            ClassUtils.isPresent(ABSTRACT_JSONP_RESPONSE_BODY_ADVICE_CLASS_NAME, WebMvcUtils.class.getClassLoader());

    /**
     * {@link RequestMappingHandlerMapping} Context name
     */
    private final static String REQUEST_MAPPING_HANDLER_MAPPING_CONTEXT_NAME = RequestMappingHandlerMapping.class.getName();

    /**
     * Any number of these characters are considered delimiters between
     * multiple values in a single init-param String value.
     *
     * @see ContextLoader#INIT_PARAM_DELIMITERS
     */
    private static final String INIT_PARAM_DELIMITERS = ",; \t\n";

    /**
     * RequestContextUtils#findWebApplicationContext(HttpServletRequest, ServletContext) method
     *
     * @since Spring 4.2.1
     */
    private static final Method findWebApplicationContextMethod;


    static {

        findWebApplicationContextMethod = findMethod(RequestContextUtils.class,
                "findWebApplicationContext", HttpServletRequest.class, ServletContext.class);

    }

//    /**
//     * Determine whether the handler findWebApplicationContextMethod is a response body findWebApplicationContextMethod.
//     *
//     * @param handlerMethod {@link HandlerMethod}
//     * @return true if the handler findWebApplicationContextMethod is a response body findWebApplicationContextMethod.
//     */
//    public static boolean isResponseBodyMethod(HandlerMethod handlerMethod) {
//        MethodParameter methodParameter = handlerMethod.getReturnType();
//        return methodParameter.getMethodAnnotation(ResponseBody.class) != null;
//    }

    /**
     * Determine whether the Bean Type is present annotated by {@link ControllerAdvice}
     *
     * @param beanType Bean Type
     * @return If {@link ControllerAdvice} bean type is present , return <code>true</code> , or <code>false</code>.
     */
    public static boolean isControllerAdviceBeanType(Class<?> beanType) {
        return AnnotationUtils.findAnnotation(beanType, ControllerAdvice.class) != null;
    }

//    /**
//     * Determine whether the handler findWebApplicationContextMethod is present annotated by {@link ControllerAdvice}
//     * when org.springframework.web.servlet.mvc.findWebApplicationContextMethod.annotation.ResponseBodyAdvice is present and
//     * can be loaded which indicates the current version of Spring Framework is 4.1 or above.
//     *
//     * @param handlerMethod {@link HandlerMethod}
//     * @return
//     */
//    public static boolean isResponseBodyAdvicePresent(HandlerMethod handlerMethod) {
//        if (ABSTRACT_JSONP_RESPONSE_BODY_ADVICE_PRESENT) {
//            Class<?> controllerType = handlerMethod.getBeanType();
//            return isControllerAdviceBeanType(controllerType);
//        }
//        return false;
//    }

    /**
     * Current {@link HttpServletRequest Request} from {@link ThreadLocal}
     *
     * @return {@link HttpServletRequest Request}
     * @throws IllegalStateException If current web environment could not be in Servlet Container with Spring Web MVC
     * @see FrameworkServlet#initContextHolders(HttpServletRequest, LocaleContext, RequestAttributes)
     * @see RequestContextFilter#initContextHolders(HttpServletRequest, ServletRequestAttributes)
     */
    public static HttpServletRequest currentRequest() throws IllegalStateException {

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        if (!(requestAttributes instanceof ServletRequestAttributes)) {

            throw new IllegalStateException("Current web environment could not be in Servlet Container " +
                    "with Spring Web MVC , because " +
                    DispatcherServlet.class.getName() +
                    " or " +
                    RequestContextFilter.class.getName() +
                    " never be used !");

        }

        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;

        return servletRequestAttributes.getRequest();

    }


    /**
     * Get the {@link WebApplicationContext} from {@link HttpServletRequest}
     *
     * @param request        {@link HttpServletRequest}
     * @param servletContext {@link ServletContext}
     * @return {@link WebApplicationContext}
     * @throws IllegalStateException if no servlet-specific context has been found
     * @see RequestContextUtils#getWebApplicationContext(ServletRequest)
     * @see DispatcherServlet#WEB_APPLICATION_CONTEXT_ATTRIBUTE
     */
    public static WebApplicationContext getWebApplicationContext(HttpServletRequest request, ServletContext servletContext) {

        WebApplicationContext webApplicationContext = null;

        if (findWebApplicationContextMethod != null) {

            try {

                webApplicationContext = (WebApplicationContext)
                        invokeMethod(findWebApplicationContextMethod, null, request, servletContext);

            } catch (IllegalStateException e) {

            }

        }

        if (webApplicationContext == null) {

            webApplicationContext = RequestContextUtils.getWebApplicationContext(request, servletContext);

        }

        return webApplicationContext;

    }

    /**
     * Get the {@link WebApplicationContext} from {@link HttpServletRequest}
     *
     * @param request {@link HttpServletRequest}
     * @return {@link WebApplicationContext}
     * @throws IllegalStateException if no servlet-specific context has been found
     * @see RequestContextUtils#getWebApplicationContext(ServletRequest)
     * @see DispatcherServlet#WEB_APPLICATION_CONTEXT_ATTRIBUTE
     */
    public static WebApplicationContext getWebApplicationContext(HttpServletRequest request) {
        return getWebApplicationContext(request, WebUtils.getServletContext(request));
    }


    /**
     * Get Current WebApplicationContext.
     *
     * @return {@link WebApplicationContext} instance.
     */
    public static WebApplicationContext currentWebApplicationContext() {

        return getWebApplicationContext(currentRequest());

    }


    /**
     * {@link RequestMappingHandlerMapping} from {@link HttpServletRequest} {@link ServletContext}
     *
     * @param request        {@link HttpServletRequest}
     * @param servletContext {@link ServletContext}
     * @return {@link RequestMappingHandlerMapping}
     */
    public static RequestMappingHandlerMapping getRequestMappingHandlerMapping(HttpServletRequest request,
                                                                               ServletContext servletContext) {


        WebApplicationContext webApplicationContext = getWebApplicationContext(request, servletContext);

        return getRequestMappingHandlerMapping(webApplicationContext);

    }

    /**
     * {@link RequestMappingHandlerMapping} from {@link HttpServletRequest} {@link ServletContext}
     *
     * @param request {@link HttpServletRequest}
     * @return {@link RequestMappingHandlerMapping}
     */
    public static RequestMappingHandlerMapping getRequestMappingHandlerMapping(HttpServletRequest request) {

        return getRequestMappingHandlerMapping(request, WebUtils.getServletContext(request));

    }

    /**
     * {@link RequestMappingHandlerMapping} from {@link WebApplicationContext}
     *
     * @param webApplicationContext {@link WebApplicationContext}
     * @return {@link RequestMappingHandlerMapping}
     */
    public static RequestMappingHandlerMapping getRequestMappingHandlerMapping(WebApplicationContext webApplicationContext) {

        RequestMappingHandlerMapping requestMappingHandlerMapping =
                BeanUtils.getOptionalBean(webApplicationContext, RequestMappingHandlerMapping.class);

        return requestMappingHandlerMapping;

    }


    /**
     * Get {@link ServletContext} in current {@link HttpServletRequest request}
     *
     * @return current request {@link ServletContext}
     * @throws IllegalStateException If current web environment could not be in Servlet Container with Spring Web MVC
     * @see RequestContextHolder#getRequestAttributes()
     * @see FrameworkServlet#initContextHolders(HttpServletRequest, LocaleContext, RequestAttributes)
     * @see RequestContextFilter#initContextHolders(HttpServletRequest, ServletRequestAttributes)
     * @see HttpServletRequest
     * @see ServletContext
     */
    public static ServletContext currentServletContext() throws IllegalStateException {

        HttpServletRequest request = currentRequest();

        ServletContext servletContext = WebUtils.getServletContext(request);

        return servletContext;

    }

    protected static String appendInitParameter(String existedParameterValue, String... parameterValues) {

        String[] existedParameterValues = StringUtils.hasLength(existedParameterValue) ?
                existedParameterValue.split(INIT_PARAM_DELIMITERS) :
                new String[0];

        List<String> parameterValuesList = new ArrayList<String>();

        if (!ObjectUtils.isEmpty(existedParameterValues)) {
            parameterValuesList.addAll(Arrays.asList(existedParameterValues));
        }

        parameterValuesList.addAll(Arrays.asList(parameterValues));

        String newParameterValue = StringUtils.arrayToDelimitedString(parameterValuesList.toArray(), ",");

        return newParameterValue;
    }

    /**
     * Append {@link ServletContext#setInitParameter(String, String) ServletContext Intialized Parameters}
     *
     * @param servletContext  {@link ServletContext}
     * @param parameterName   the name of init parameter
     * @param parameterValues the values of init parameters
     */
    public static void appendInitParameters(ServletContext servletContext, String parameterName, String... parameterValues) {

        Assert.notNull(servletContext);
        Assert.hasLength(parameterName);
        Assert.notNull(parameterValues);

        String existedParameterValue = servletContext.getInitParameter(parameterName);

        String newParameterValue = appendInitParameter(existedParameterValue, parameterValues);

        if (StringUtils.hasLength(newParameterValue)) {
            servletContext.setInitParameter(parameterName, newParameterValue);
        }

    }

    /**
     * Append  initialized parameter for {@link ApplicationContextInitializer Global Initializer Class}
     *
     * @param servletContext          {@link ServletContext}
     * @param contextInitializerClass the class of {@link ApplicationContextInitializer}
     * @see ContextLoader#GLOBAL_INITIALIZER_CLASSES_PARAM
     */
    public static void appendGlobalInitializerClassInitParameter(ServletContext servletContext,
                                                                 Class<? extends ApplicationContextInitializer> contextInitializerClass) {

        String contextInitializerClassName = contextInitializerClass.getName();

        appendInitParameters(servletContext, GLOBAL_INITIALIZER_CLASSES_PARAM, contextInitializerClassName);

    }

    /**
     * Append  initialized parameter for {@link ApplicationContextInitializer Context Initializer Class}
     *
     * @param servletContext          {@link ServletContext}
     * @param contextInitializerClass the class of {@link ApplicationContextInitializer}
     * @see ContextLoader#CONTEXT_INITIALIZER_CLASSES_PARAM
     */
    public static void appendContextInitializerClassInitParameter(ServletContext servletContext,
                                                                  Class<? extends ApplicationContextInitializer> contextInitializerClass) {

        String contextInitializerClassName = contextInitializerClass.getName();

        appendInitParameters(servletContext, CONTEXT_INITIALIZER_CLASSES_PARAM, contextInitializerClassName);

    }


    /**
     * Append initialized parameter for {@link ApplicationContextInitializer Context Initializer Class} into {@link
     * FrameworkServlet}
     *
     * @param servletContext          {@link ServletContext}
     * @param contextInitializerClass the class of {@link ApplicationContextInitializer}
     * @see FrameworkServlet#applyInitializers(ConfigurableApplicationContext)
     */
    public static void appendFrameworkServletContextInitializerClassInitParameter(
            ServletContext servletContext,
            Class<? extends ApplicationContextInitializer> contextInitializerClass) {

        Collection<? extends ServletRegistration> servletRegistrations =
                WebUtils.findServletRegistrations(servletContext, FrameworkServlet.class).values();

        for (ServletRegistration servletRegistration : servletRegistrations) {
            String contextInitializerClassName = servletRegistration.getInitParameter(CONTEXT_INITIALIZER_CLASSES_PARAM);
            String newContextInitializerClassName = appendInitParameter(contextInitializerClassName, contextInitializerClass.getName());
            servletRegistration.setInitParameter(CONTEXT_INITIALIZER_CLASSES_PARAM, newContextInitializerClassName);
        }

    }

    /**
     * Is page render request
     *
     * @param modelAndView {@link ModelAndView}
     * @return If current request is for page render , return <code>true</code> , or <code>false</code>.
     */
    public static boolean isPageRenderRequest(ModelAndView modelAndView) {

        if (modelAndView != null) {

            String viewName = modelAndView.getViewName();

            return StringUtils.hasText(viewName);

        }

        return false;

    }


}