package ru.yandex.webmaster.common.http;

import java.io.BufferedReader;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.IdentityHashMap;

import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.TreeTraversingParser;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.joda.time.DateTime;
import org.joda.time.Instant;

import ru.yandex.webmaster.common.WebmasterActionException;
import ru.yandex.webmaster.common.WebmasterExceptionType;

/**
 * @author aherman
 */
public class RequestConverter {
    public static final String MIME_APPLICATION_JSON = "application/json";
    public static final String MIME_MULTIPART_FORM_DATA = "multipart/form-data";
    public static final String MIME_APPLICATION_OCTET_STREAM = "application/octet-stream";

    private static final ObjectMapper OM = new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    private final IdentityHashMap<Class, ParameterConverter> converters;
    private final ParameterConverter enumParameterConverter = new EnumParameterConverter();

    public RequestConverter(IdentityHashMap<Class, ParameterConverter> converters) {
        this.converters = converters;
        fillDefaultConverter();
    }

    void fillDefaultConverter() {
        PrimitiveParameterConverter primitiveParameterConverter = new PrimitiveParameterConverter();

        addConverterIfAbsent(String.class, primitiveParameterConverter);

        addConverterIfAbsent(Boolean.class, primitiveParameterConverter);
        addConverterIfAbsent(Boolean.TYPE, primitiveParameterConverter);

        addConverterIfAbsent(Integer.class, primitiveParameterConverter);
        addConverterIfAbsent(Integer.TYPE, primitiveParameterConverter);

        addConverterIfAbsent(Long.class, primitiveParameterConverter);
        addConverterIfAbsent(Long.TYPE, primitiveParameterConverter);

        addConverterIfAbsent(Float.class, primitiveParameterConverter);
        addConverterIfAbsent(Float.TYPE, primitiveParameterConverter);

        addConverterIfAbsent(Double.class, primitiveParameterConverter);
        addConverterIfAbsent(Double.TYPE, primitiveParameterConverter);

        DateTimeParameterConverter dateTimeParameterConverter = new DateTimeParameterConverter();
        addConverterIfAbsent(Instant.class, dateTimeParameterConverter);
        addConverterIfAbsent(DateTime.class, dateTimeParameterConverter);
    }

    void addConverterIfAbsent(Class<?> clazz, ParameterConverter converter) {
        if (converters.containsKey(clazz)) {
            return;
        }
        converters.put(clazz, converter);
    }

    public void fillRequest(Object requestObject, HttpServletRequest httpRequest)
            throws WebmasterActionException
    {
        JsonNode jsonNode = null;
        if (StringUtils.startsWith(httpRequest.getContentType(), MIME_APPLICATION_JSON)) {
            BufferedReader reader = null;
            try {
                reader = httpRequest.getReader();
                jsonNode = OM.readTree(reader);
            } catch (IOException e) {
                throw new WebmasterActionException(WebmasterExceptionType.INTERNAL__UNABLE_TO_READ_JSON_REQUEST,
                        "Unable to read json request data", e);
            } finally {
                IOUtils.closeQuietly(reader);
            }
        }
        boolean isMultipart = StringUtils.startsWith(httpRequest.getContentType(), MIME_MULTIPART_FORM_DATA);
        Class<?> requestClass = requestObject.getClass();
        fillWithClass(requestObject, httpRequest, jsonNode, requestClass, isMultipart);
        Class<?>[] interfaces = requestClass.getInterfaces();
        for (Class<?> anInterface : interfaces) {
            fillWithClass(requestObject, httpRequest, jsonNode, anInterface, isMultipart);
        }
        if (requestObject instanceof BinaryActionRequest) {
//            if (StringUtils.startsWith(httpRequest.getContentType(), MIME_APPLICATION_OCTET_STREAM)) {
                try {
                    ServletInputStream inputStream = httpRequest.getInputStream();
                    ((BinaryActionRequest) requestObject).setContentInputStream(inputStream);
                } catch (IOException e) {
                    throw new WebmasterActionException(WebmasterExceptionType.INTERNAL__UNABLE_TO_READ_REQUEST_BINARY_DATA,
                            "Unable to read request content", e);
                }
//            }
        }
    }

    private void fillWithClass(Object requestObject, HttpServletRequest httpRequest, JsonNode jsonData,
            Class<?> requestClass, boolean multipart)
            throws WebmasterActionException
    {
        Method[] methods = requestClass.getDeclaredMethods();
        for (Method method : methods) {
            String parameterName = getParamName(method);
            boolean parameterIsRequired = false;
            Class<?> parameterType = null;

            RequestFileProperty requestFileProperty = method.getAnnotation(RequestFileProperty.class);
            if (requestFileProperty != null) {
                parameterIsRequired = requestFileProperty.required();
                parameterType = checkAndGetParameterType(method);
                if (parameterType != FileParameter.class) {
                    throw new WebmasterActionException(WebmasterExceptionType.INTERNAL__UNSUPPORTED_REQUEST_PARAMETER_TYPE,
                            "Parameter type must be file: name=" + parameterName + " class=" + parameterType,
                            "name", parameterName);
                }
                if (multipart) {
                    try {
                        Part part = httpRequest.getPart(parameterName);
                        if (part != null) {
                            FileParameter fileParameter = new FileParameter(part);
                            if (requestFileProperty.maxSize() > 0) {
                                if (part.getSize() > requestFileProperty.maxSize()) {
                                    throw new WebmasterActionException(WebmasterExceptionType.REQUEST__ILLEGAL_FILE_PARAMETER,
                                            "File size limit exceeded : name=" + parameterName + " class=" + parameterType
                                                    + " maxSize=" + requestFileProperty.maxSize(),
                                            "name", parameterName);
                                }
                            }
                            method.invoke(requestObject, fileParameter);
                        }
                    } catch (IOException | ServletException e) {
                        throw new WebmasterActionException(WebmasterExceptionType.REQUEST__ILLEGAL_PARAMETER_VALUE,
                                "Unable to read file parameter: name=" + parameterName + " class=" + parameterType,
                                e, "name", parameterName);
                    } catch (InvocationTargetException | IllegalAccessException e) {
                        throw new WebmasterActionException(WebmasterExceptionType.REQUEST__ILLEGAL_FILE_PARAMETER,
                                "Unable to set file parameter: name=" + parameterName + " class=" + parameterType,
                                "name", parameterName);
                    }
                    continue;
                }
            } else {
                RequestQueryProperty requestQueryProperty = method.getAnnotation(RequestQueryProperty.class);
                if (requestQueryProperty != null) {
                    parameterIsRequired = requestQueryProperty.required();
                    parameterType = checkAndGetParameterType(method);

                    if (parameterType.isArray() || parameterType.isInstance(Collection.class)) {
                        throw new WebmasterActionException(
                                WebmasterExceptionType.INTERNAL__UNSUPPORTED_REQUEST_PARAMETER_TYPE,
                                "Arrays or collections are not supported in get parameters");
                    }
                    if(setValueFromQuery(requestObject, method, parameterName, parameterType, httpRequest)) {
                        continue;
                    }
                }

                RequestPostProperty requestPostProperty = method.getAnnotation(RequestPostProperty.class);
                if (requestPostProperty != null) {
                    parameterIsRequired = parameterIsRequired || requestPostProperty.required();
                    if (parameterType == null) {
                        parameterType = checkAndGetParameterType(method);
                    }
                    if (jsonData != null && setValueFromJson(requestObject, method, parameterName, parameterType, jsonData)) {
                        continue;
                    }
                }
            }

            if (parameterIsRequired) {
                throw new WebmasterActionException(WebmasterExceptionType.REQUEST__REQUIRED_PARAMETER_MISSING,
                        "Required parameter missing: name=" + parameterName + " class=" + parameterType,
                        "name", parameterName);
            }
        }
    }

    private boolean setValueFromJson(Object requestObject, Method method, String parameterName,
            Class<?> parameterType, JsonNode jsonData)
    {
        Object value = extractJsonValue(parameterName, parameterType, jsonData);
        if (value == null) {
            return false;
        }
        try {
            method.invoke(requestObject, value);
            return true;
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new WebmasterActionException(WebmasterExceptionType.REQUEST__ILLEGAL_PARAMETER_VALUE,
                    "Unable to set parameter: name=" + parameterName, e,
                    "name", parameterName);
        }
    }

    private Object extractJsonValue(String parameterName, Class<?> parameterType, JsonNode jsonData) {
        JsonNode valueNode = jsonData.get(parameterName);
        if (valueNode == null || valueNode.isNull()) {
            return null;
        }

        if(valueNode.isValueNode()) {
            return convertValue(parameterName, parameterType, valueNode.asText());
        }

        if (parameterType.isArray()) {
            return getArray(parameterName, parameterType, valueNode);
        }

        if (valueNode.isObject()) {
            try {
                return OM.readValue(new TreeTraversingParser(valueNode), parameterType);
            } catch (IOException e) {
                throw new WebmasterActionException(WebmasterExceptionType.REQUEST__ILLEGAL_PARAMETER_VALUE,
                        "Unable to set parameter: name=" + parameterName, e,
                        "name", parameterName);
            }
        }

        throw new WebmasterActionException(WebmasterExceptionType.REQUEST__ILLEGAL_PARAMETER_VALUE,
                "Unknown parameter type: name=" + parameterName,
                "name", parameterName);
    }

    private Object getArray(String parameterName, Class<?> parameterType, JsonNode jsonData) {
        if (!jsonData.isArray()) {
            return null;
        }
        ArrayNode arrayData = (ArrayNode) jsonData;
        Class<?> componentType = parameterType.getComponentType();
        Object result = Array.newInstance(componentType, arrayData.size());

        if(isConvertible(componentType)) {
            for (int i = 0; i < arrayData.size(); i++) {
                Object value = convertValue(parameterName, componentType, arrayData.get(i).asText());
                Array.set(result, i, value);
            }
            return result;
        }

        for(int i = 0; i < arrayData.size(); i++) {
            try {
                Object value = OM.readValue(new TreeTraversingParser(arrayData.get(i)), componentType);
                Array.set(result, i, value);
            } catch (IOException e) {
                throw new WebmasterActionException(WebmasterExceptionType.REQUEST__ILLEGAL_PARAMETER_VALUE,
                        "Unable to set parameter: name=" + parameterName, e,
                        "name", parameterName);
            }
        }

        return result;
    }

    private static Class<?> checkAndGetParameterType(Method method) {
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (parameterTypes.length != 1) {
            throw new WebmasterActionException(WebmasterExceptionType.INTERNAL__UNABLE_TO_INSTANTIATE_REQUEST,
                    "Setter must have only one parameter");
        }
        return parameterTypes[0];
    }

    private boolean setValueFromQuery(Object requestObject, Method method, String parameterName, Class<?> parameterType,
            HttpServletRequest httpRequest)
            throws WebmasterActionException
    {
        String valueStr = StringUtils.trimToNull(httpRequest.getParameter(parameterName));
        if (valueStr == null) {
            return false;
        }

        Object value = convertValue(parameterName, parameterType, valueStr);

        try {
            method.invoke(requestObject, value);
            return true;
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new WebmasterActionException(WebmasterExceptionType.REQUEST__ILLEGAL_PARAMETER_VALUE,
                    "Unable to set parameter: name=" + parameterName, e,
                    "name", parameterName, "value", valueStr);
        }
    }

    private boolean isConvertible(Class<?> parameterType) {
        if (parameterType.isEnum()) {
            return true;
        }
        return converters.containsKey(parameterType);
    }

    @NotNull
    private Object convertValue(String parameterName, Class<?> parameterType, String valueStr) {
        Object value;
        if (parameterType.isEnum()) {
            value = enumParameterConverter.convert(parameterName, valueStr, parameterType);
        } else {
            ParameterConverter converter = converters.get(parameterType);
            if (converter == null) {
                throw new WebmasterActionException(WebmasterExceptionType.INTERNAL__UNSUPPORTED_REQUEST_PARAMETER_TYPE,
                        "Unsupported parameter type: name=" + parameterName,
                        "name", parameterName, "value", valueStr);
            }
            value = converter.convert(parameterName, valueStr, parameterType);
        }

        if (value == null) {
            throw new WebmasterActionException(WebmasterExceptionType.REQUEST__ILLEGAL_PARAMETER_VALUE,
                    "Unable to convert parameter: name=" + parameterName,
                    "name", parameterName, "value", valueStr);
        }
        return value;
    }

    private static String getParamName(Method method) {
        String name = method.getName();
        if (name.startsWith("set")) {
            char firstChar = Character.toLowerCase(name.charAt(3));
            name = firstChar + name.substring(4);
        }

        return name;
    }
}
