package org.jboss.resteasy.wadl;

import org.jboss.resteasy.wadl.i18n.LogMessages;
import org.jboss.resteasy.wadl.i18n.Messages;
import org.jboss.resteasy.wadl.jaxb.*;

import javax.ws.rs.core.MediaType;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.namespace.QName;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;

import static org.jboss.resteasy.wadl.ResteasyWadlMethodParamMetaData.MethodParamType.*;

/**
 * @author <a href="mailto:l.weinan@gmail.com">Weinan Li</a>
 */
public class ResteasyWadlWriter {

    public byte[] getBytes(String base, Map<String, ResteasyWadlServiceRegistry> serviceRegistries) throws JAXBException {
        StringWriter stringWriter = getStringWriter(base, serviceRegistries);
        return stringWriter.toString().getBytes();
    }

    public StringWriter getStringWriter(String base, Map<String, ResteasyWadlServiceRegistry> serviceRegistries) throws JAXBException {
        ObjectFactory factory = new ObjectFactory();
        Application app = factory.createApplication();
        JAXBContext context = JAXBContext.newInstance(Application.class);
        Marshaller marshaller = context.createMarshaller();

        for (Map.Entry<String, ResteasyWadlServiceRegistry> entry : serviceRegistries.entrySet()) {
            String uri = base;
            if (entry.getKey() != null) uri += entry.getKey();
            Resources resources = new Resources();
            resources.setBase(uri);
            app.getResources().add(resources);
            processWadl(entry.getValue(), resources);
        }

        StringWriter stringWriter = new StringWriter();
        PrintWriter writer = new PrintWriter(stringWriter);
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        marshaller.marshal(app, writer);
        return stringWriter;
    }

    private void processWadl(ResteasyWadlServiceRegistry serviceRegistry, Resources root) throws JAXBException {

        for (Map.Entry<String, ResteasyWadlResourceMetaData> resourceMetaDataEntry : serviceRegistry.getResources().entrySet()) {
            LogMessages.LOGGER.debug(Messages.MESSAGES.path(resourceMetaDataEntry.getKey()));
            Resource resourceClass = new Resource();

            resourceClass.setPath(resourceMetaDataEntry.getKey());
            root.getResource().add(resourceClass);

            for (ResteasyWadlMethodMetaData methodMetaData : resourceMetaDataEntry.getValue().getMethodsMetaData()) {
                Method method = new Method();

                // First we need to check whether @Path annotation exists in a method.
                // If the @Path annotation exists, we need to create a resource for it.
                if (methodMetaData.getMethodUri() != null) {
                    Resource methodResource = new Resource();
                    methodResource.setPath(methodMetaData.getMethodUri());
                    methodResource.getMethodOrResource().add(method);
                    resourceClass.getMethodOrResource().add(methodResource);
                    // add params into method resource
                    processMethodParams(methodResource, methodMetaData, method);
                } else {
                    // register method into resource
                    resourceClass.getMethodOrResource().add(method);
                    // we need to check whether the method have parameters or not.
                    // params belong to the resource of the method
                    processMethodParams(resourceClass, methodMetaData, method);
                }

                // method name = {GET, POST, DELETE, ...}
                for (String name : methodMetaData.getHttpMethods()) {
                    method.setName(name);
                }

                // method id = method name
                method.setId(methodMetaData.getMethod().getName());

                // process response of method
                Response response = createResponse(serviceRegistry, methodMetaData);
                method.getResponse().add(response);
            }
        }

        for (ResteasyWadlServiceRegistry subService : serviceRegistry.getLocators())
            processWadl(subService, root);
    }

    private void processMethodParams(Resource currentResourceClass, ResteasyWadlMethodMetaData methodMetaData, Method method) {
        // process method parameters
        Request request = new Request();

        for (ResteasyWadlMethodParamMetaData paramMetaData : methodMetaData.getParameters()) {
            Param param = createParam(currentResourceClass, method, paramMetaData, request);
        }
    }

    private Response createResponse(ResteasyWadlServiceRegistry serviceRegistry, ResteasyWadlMethodMetaData methodMetaData) {
        Response response = new Response();

        Class _type = methodMetaData.getMethod().getReturnType();
        Type _generic = methodMetaData.getMethod().getGenericReturnType();

        MediaType mediaType = MediaType.WILDCARD_TYPE;

        if (methodMetaData.getProduces() != null) {
            for (String produces : methodMetaData.getProduces()) {
                for (String _produces : produces.split(",")) {
                    mediaType = MediaType.valueOf(_produces);
                    if (mediaType == null) {
                        mediaType = serviceRegistry.getProviderFactory().getConcreteMediaTypeFromMessageBodyWriters(_type, _generic, methodMetaData.getMethod().getAnnotations(), MediaType.WILDCARD_TYPE);
                        if (mediaType == null)
                            mediaType = MediaType.WILDCARD_TYPE;
                    }
                    Representation representation = createRepresentation(mediaType);
                    response.getRepresentation().add(representation);
                }
            }
        }
        return response;
    }

    private Param createParam(Resource currentResourceClass, Method method, ResteasyWadlMethodParamMetaData paramMetaData, Request request) {
        Param param = new Param();
        setType(param, paramMetaData);

        // All the method's @PathParam belong to resource
        if (paramMetaData.getParamType().equals(PATH_PARAMETER)) {
            param.setStyle(ParamStyle.TEMPLATE);
            param.setName(paramMetaData.getParamName());
            currentResourceClass.getParam().add(param);
        } else if (paramMetaData.getParamType().equals(COOKIE_PARAMETER)) {
            param.setStyle(ParamStyle.HEADER);
            request.getParam().add(param);
            param.setName("Cookie");
            param.setPath(paramMetaData.getParamName());
            method.setRequest(request);
        } else if (paramMetaData.getParamType().equals(HEADER_PARAMETER)) {
            param.setStyle(ParamStyle.HEADER);
            request.getParam().add(param);
            param.setName(paramMetaData.getParamName());
            method.setRequest(request);
        } else if (paramMetaData.getParamType().equals(MATRIX_PARAMETER)) {
            param.setStyle(ParamStyle.MATRIX);
            param.setName(paramMetaData.getParamName());
            currentResourceClass.getParam().add(param);
        } else if (paramMetaData.getParamType().equals(QUERY_PARAMETER)) {
            param.setStyle(ParamStyle.QUERY);
            request.getParam().add(param);
            param.setName(paramMetaData.getParamName());
            method.setRequest(request);
        } else if (paramMetaData.getParamType().equals(FORM_PARAMETER)) {
            param.setStyle(ParamStyle.QUERY);
            Representation formRepresentation = createFormRepresentation(request);
            param.setName(paramMetaData.getParamName());
            formRepresentation.getParam().add(param);
            method.setRequest(request);
        } else if (paramMetaData.getParamType().equals(FORM)) {
            param.setStyle(ParamStyle.QUERY);
            Representation formRepresentation = createFormRepresentation(request);
            param.setName(paramMetaData.getParamName());
            formRepresentation.getParam().add(param);
            method.setRequest(request);
        }
        return param;
    }

    private Representation createFormRepresentation(Request request) {
        Representation formRepresentation = getRepresentationByMediaType(request.getRepresentation(),
                MediaType.APPLICATION_FORM_URLENCODED_TYPE);
        if (formRepresentation == null) {
            formRepresentation = createRepresentation(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
            request.getRepresentation().add(formRepresentation);
        }
        return formRepresentation;
    }

    private Representation createRepresentation(MediaType mediaType) {
        Representation representation;
        representation = new Representation();
        representation.setMediaType(mediaType.toString());
        return representation;
    }

    private Representation getRepresentationByMediaType(
            final List<Representation> representations, MediaType mediaType) {
        for (Representation representation : representations) {
            if (mediaType.toString().equals(representation.getMediaType())) {
                return representation;
            }
        }
        return null;
    }

    private void setType(Param param, ResteasyWadlMethodParamMetaData paramMetaData) {
        if (paramMetaData.getType().equals(int.class) || paramMetaData.getType().equals(Integer.class)) {
            param.setType(new QName("http://www.w3.org/2001/XMLSchema", "int", "xs"));
        } else if (paramMetaData.getType().equals(boolean.class) || paramMetaData.getType().equals(Boolean.class)) {
            param.setType(new QName("http://www.w3.org/2001/XMLSchema", "boolean", "xs"));
        } else if (paramMetaData.getType().equals(long.class) || paramMetaData.getType().equals(Long.class)) {
            param.setType(new QName("http://www.w3.org/2001/XMLSchema", "long", "xs"));
        } else if (paramMetaData.getType().equals(short.class) || paramMetaData.getType().equals(Short.class)) {
            param.setType(new QName("http://www.w3.org/2001/XMLSchema", "short", "xs"));
        } else if (paramMetaData.getType().equals(byte.class) || paramMetaData.getType().equals(Byte.class)) {
            param.setType(new QName("http://www.w3.org/2001/XMLSchema", "byte", "xs"));
        } else if (paramMetaData.getType().equals(float.class) || paramMetaData.getType().equals(Float.class)) {
            param.setType(new QName("http://www.w3.org/2001/XMLSchema", "float", "xs"));
        } else if (paramMetaData.getType().equals(double.class) || paramMetaData.getType().equals(Double.class)) {
            param.setType(new QName("http://www.w3.org/2001/XMLSchema", "double", "xs"));
        } else if (paramMetaData.getType().equals(Map.class) || paramMetaData.getType().equals(List.class)) {
            param.setType(new QName("http://www.w3.org/2001/XMLSchema", "complex", "xs"));
        } else {
            param.setType(new QName("http://www.w3.org/2001/XMLSchema", "string", "xs"));
        }
    }
}
