package com.blade.mvc.handler;

import com.blade.exception.BladeException;
import com.blade.kit.JsonKit;
import com.blade.kit.ReflectKit;
import com.blade.kit.StringKit;
import com.blade.mvc.RouteContext;
import com.blade.mvc.annotation.*;
import com.blade.mvc.http.HttpSession;
import com.blade.mvc.http.Request;
import com.blade.mvc.http.Response;
import com.blade.mvc.http.Session;
import com.blade.mvc.multipart.FileItem;
import com.blade.mvc.ui.ModelAndView;
import com.blade.reflectasm.ASMUtils;

import java.lang.reflect.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Route method param parse
 *
 * @author biezhi
 * 2017/9/20
 */
public final class RouteActionArguments {

    public static Object[] getRouteActionParameters(RouteContext context) {
        Method  actionMethod = context.routeAction();
        Request request      = context.request();
        actionMethod.setAccessible(true);

        Parameter[] parameters     = actionMethod.getParameters();
        Object[]    args           = new Object[parameters.length];
        String[]    parameterNames = ASMUtils.findMethodParmeterNames(actionMethod);

        for (int i = 0, len = parameters.length; i < len; i++) {
            Parameter parameter = parameters[i];
            String    paramName = Objects.requireNonNull(parameterNames)[i];
            Type      argType   = parameter.getParameterizedType();
            if (containsAnnotation(parameter)) {
                args[i] = getAnnotationParam(parameter, paramName, request);
                continue;
            }
            if (ReflectKit.isBasicType(argType)) {
                args[i] = request.query(paramName);
                continue;
            }
            args[i] = getCustomType(parameter, paramName, context);
        }
        return args;
    }

    private static boolean containsAnnotation(Parameter parameter) {
        return parameter.getAnnotation(PathParam.class) != null ||
                parameter.getAnnotation(Param.class) != null ||
                parameter.getAnnotation(HeaderParam.class) != null ||
                parameter.getAnnotation(BodyParam.class) != null ||
                parameter.getAnnotation(CookieParam.class) != null ||
                parameter.getAnnotation(MultipartParam.class) != null;
    }

    private static Object getCustomType(Parameter parameter, String paramName, RouteContext context) {
        Type argType = parameter.getParameterizedType();
        if (argType == RouteContext.class) {
            return context;
        } else if (argType == Request.class) {
            return context.request();
        } else if (argType == Response.class) {
            return context.response();
        } else if (argType == Session.class || argType == HttpSession.class) {
            return context.request().session();
        } else if (argType == FileItem.class) {
            return new ArrayList<>(context.request().fileItems().values()).get(0);
        } else if (argType == ModelAndView.class) {
            return new ModelAndView();
        } else if (argType == Map.class) {
            return context.request().parameters();
        } else if (argType == Optional.class) {
            ParameterizedType firstParam           = (ParameterizedType) parameter.getParameterizedType();
            Type              paramsOfFirstGeneric = firstParam.getActualTypeArguments()[0];
            Class<?>          modelType            = ReflectKit.form(paramsOfFirstGeneric.getTypeName());
            return Optional.ofNullable(parseModel(modelType, context.request(), null));
        } else if (ParameterizedType.class.isInstance(argType)) {
            String       name   = parameter.getName();
            List<String> values = context.request().parameters().get(name);
            return getParameterizedTypeValues(values, argType);
        } else if (ReflectKit.isArray(argType)) {
            List<String> values = context.request().parameters().get(paramName);
            if (null == values) {
                return null;
            }
            Class  arrayCls = (Class) argType;
            Object aObject  = Array.newInstance(arrayCls.getComponentType(), values.size());
            for (int i = 0; i < values.size(); i++) {
                Array.set(aObject, i, ReflectKit.convert(arrayCls.getComponentType(), values.get(i)));
            }
            return aObject;
        } else {
            return parseModel(ReflectKit.typeToClass(argType), context.request(), null);
        }
    }

    private static Object getAnnotationParam(Parameter parameter, String paramName, Request request) {
        Type  argType = parameter.getParameterizedType();
        Param param   = parameter.getAnnotation(Param.class);

        ParamStruct.ParamStructBuilder structBuilder = ParamStruct.builder().argType(argType).request(request);

        if (null != param) {
            ParamStruct paramStruct = structBuilder.param(param).paramName(paramName).build();
            return getQueryParam(paramStruct);
        }
        BodyParam bodyParam = parameter.getAnnotation(BodyParam.class);
        if (null != bodyParam) {
            return getBodyParam(structBuilder.build());
        }
        PathParam pathParam = parameter.getAnnotation(PathParam.class);
        if (null != pathParam) {
            return getPathParam(structBuilder.pathParam(pathParam).paramName(paramName).build());
        }
        HeaderParam headerParam = parameter.getAnnotation(HeaderParam.class);
        if (null != headerParam) {
            return getHeader(structBuilder.headerParam(headerParam).paramName(paramName).build());
        }
        // cookie param
        CookieParam cookieParam = parameter.getAnnotation(CookieParam.class);
        if (null != cookieParam) {
            return getCookie(structBuilder.cookieParam(cookieParam).paramName(paramName).build());
        }
        // form multipart
        MultipartParam multipartParam = parameter.getAnnotation(MultipartParam.class);
        if (null != multipartParam && argType == FileItem.class) {
            String name = StringKit.isBlank(multipartParam.value()) ? paramName : multipartParam.value();
            return request.fileItem(name).orElse(null);
        }
        return null;
    }

    private static Object getBodyParam(ParamStruct paramStruct) {
        Type    argType = paramStruct.argType;
        Request request = paramStruct.request;

        if (ReflectKit.isPrimitive(argType)) {
            return ReflectKit.convert(argType, request.bodyToString());
        } else {
            String json = request.bodyToString();
            return StringKit.isNotEmpty(json) ? JsonKit.formJson(json, argType) : null;
        }
    }

    private static Object getQueryParam(ParamStruct paramStruct) {
        Param   param     = paramStruct.param;
        String  paramName = paramStruct.paramName;
        Type    argType   = paramStruct.argType;
        Request request   = paramStruct.request;
        if (null == param) {
            return null;
        }

        String name = StringKit.isBlank(param.name()) ? paramName : param.name();

        if (ReflectKit.isBasicType(argType) || argType.equals(Date.class)
                || argType.equals(BigDecimal.class) || argType.equals(LocalDate.class)
                || argType.equals(LocalDateTime.class) || (argType instanceof Class && ((Class) argType).isEnum())) {

            String value = request.query(name).orElseGet(() -> getDefaultValue(param.defaultValue(), argType));

            return ReflectKit.convert(argType, value);
        } else {
            if (ParameterizedType.class.isInstance(argType)) {

                List<String> values = request.parameters().get(param.name());
                return getParameterizedTypeValues(values, argType);
            }
            return parseModel(ReflectKit.typeToClass(argType), request, param.name());
        }
    }

    private static String getDefaultValue(String defaultValue, Type argType) {
        if (argType.equals(String.class)) {
            if (StringKit.isNotEmpty(defaultValue)) {
                return defaultValue;
            }
            return null;
        }
        if ("".equals(defaultValue) && ReflectKit.isBasicType(argType)) {

            if (argType.equals(int.class) || argType.equals(long.class)
                    || argType.equals(double.class) || argType.equals(float.class)
                    || argType.equals(short.class) || argType.equals(byte.class)) {

                return "0";
            }

            if (argType.equals(boolean.class)) {
                return "false";
            }
            return "";
        }
        return defaultValue;
    }

    private static Object getCookie(ParamStruct paramStruct) throws BladeException {
        Type        argType     = paramStruct.argType;
        CookieParam cookieParam = paramStruct.cookieParam;
        String      paramName   = paramStruct.paramName;
        Request     request     = paramStruct.request;

        String cookieName = StringKit.isEmpty(cookieParam.value()) ? paramName : cookieParam.value();
        String val        = request.cookie(cookieName);
        if (null == val) {
            val = cookieParam.defaultValue();
        }
        return ReflectKit.convert(argType, val);
    }

    private static Object getHeader(ParamStruct paramStruct) throws BladeException {
        Type        argType     = paramStruct.argType;
        HeaderParam headerParam = paramStruct.headerParam;
        String      paramName   = paramStruct.paramName;
        Request     request     = paramStruct.request;

        String key = StringKit.isEmpty(headerParam.value()) ? paramName : headerParam.value();
        String val = request.header(key);
        if (StringKit.isBlank(val)) {
            val = headerParam.defaultValue();
        }
        return ReflectKit.convert(argType, val);
    }

    private static Object getPathParam(ParamStruct paramStruct) {
        Type      argType   = paramStruct.argType;
        PathParam pathParam = paramStruct.pathParam;
        String    paramName = paramStruct.paramName;
        Request   request   = paramStruct.request;

        String name = StringKit.isEmpty(pathParam.name()) ? paramName : pathParam.name();
        String val  = request.pathString(name);
        if (StringKit.isBlank(val)) {
            val = pathParam.defaultValue();
        }
        return ReflectKit.convert(argType, val);
    }

    public static <T> T parseModel(Class<T> argType, Request request, String name) {

        T obj = ReflectKit.newInstance(argType);

        List<Field> fields = ReflectKit.loopFields(argType);

        for (Field field : fields) {
            if ("serialVersionUID".equals(field.getName())) {
                continue;
            }
            Object value = null;

            Optional<String> fieldValue = request.query(field.getName());

            if (StringKit.isNotBlank(name)) {
                String fieldName = name + "[" + field.getName() + "]";
                fieldValue = request.query(fieldName);
            }

            if (fieldValue.isPresent() && StringKit.isNotBlank(fieldValue.get())) {
                value = ReflectKit.convert(field.getType(), fieldValue.get());
            }
            if (null != value) {
                ReflectKit.setFieldValue(field, obj, value);
            }
        }
        return obj;
    }

    private static Object getParameterizedTypeValues(List<String> values, Type argType) {
        if (null == values || null == argType) {
            return null;
        }
        ParameterizedType parameterizedType = (ParameterizedType) argType;
        Class<?>          realType          = (Class) parameterizedType.getActualTypeArguments()[0];
        return values.stream()
                .map(s -> ReflectKit.convert(realType, s))
                .collect(Collectors.toList());
    }
}