/*
 * Copyright (C) 2016 Red Hat, inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */
package org.jboss.as.jaxrs;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM;
import static org.wildfly.extension.undertow.DeploymentDefinition.CONTEXT_ROOT;
import static org.wildfly.extension.undertow.DeploymentDefinition.SERVER;
import static org.wildfly.extension.undertow.DeploymentDefinition.VIRTUAL_HOST;
import io.undertow.servlet.api.ThreadSetupAction.Handle;
import io.undertow.servlet.handlers.ServletHandler;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.ws.rs.CookieParam;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.MatrixParam;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import org.jboss.as.controller.AttributeDefinition;
import org.jboss.as.controller.ObjectListAttributeDefinition;
import org.jboss.as.controller.ObjectTypeAttributeDefinition;
import org.jboss.as.controller.OperationContext;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.OperationStepHandler;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
import org.jboss.as.controller.SimpleListAttributeDefinition;
import org.jboss.as.controller.SimpleResourceDefinition;
import org.jboss.as.controller.registry.ManagementResourceRegistration;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
import org.jboss.msc.service.ServiceController;
import org.jboss.resteasy.core.ResourceInvoker;
import org.jboss.resteasy.core.ResourceLocatorInvoker;
import org.jboss.resteasy.core.ResourceMethodInvoker;
import org.jboss.resteasy.core.ResourceMethodRegistry;
import org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher;
import org.jboss.resteasy.spi.metadata.ResourceBuilder;
import org.jboss.resteasy.spi.metadata.ResourceClass;
import org.jboss.resteasy.spi.metadata.ResourceLocator;
import org.jboss.resteasy.spi.metadata.ResourceMethod;
import org.wildfly.extension.undertow.UndertowExtension;
import org.wildfly.extension.undertow.UndertowService;
import org.wildfly.extension.undertow.deployment.UndertowDeploymentService;

/**
 * @author <a href="mailto:lgao@redhat.com">Lin Gao</a>
 */
@SuppressWarnings("deprecation")
public class DeploymentRestResourcesDefintion extends SimpleResourceDefinition {

    public static DeploymentRestResourcesDefintion INSTANCE = new DeploymentRestResourcesDefintion();

    public static final String REST_RESOURCE_NAME = "rest-resource";

    public static final AttributeDefinition RESOURCE_CLASS = new SimpleAttributeDefinitionBuilder("resource-class",
            ModelType.STRING, true).setStorageRuntime().build();

    public static final AttributeDefinition RESOURCE_PATH = new SimpleAttributeDefinitionBuilder("resource-path", ModelType.STRING, true)
            .setStorageRuntime().build();

    public static final AttributeDefinition RESOURCE_METHOD = new SimpleAttributeDefinitionBuilder("resource-method",
            ModelType.STRING, false).setStorageRuntime().build();

    public static final AttributeDefinition RESOURCE_METHODS = new SimpleListAttributeDefinition.Builder("resource-methods", RESOURCE_METHOD)
            .setStorageRuntime().build();

    public static final AttributeDefinition CONSUME = new SimpleAttributeDefinitionBuilder("consume", ModelType.STRING, true)
            .setStorageRuntime().build();

    public static final AttributeDefinition CONSUMES = new SimpleListAttributeDefinition.Builder("consumes", CONSUME)
            .setStorageRuntime().build();

    public static final AttributeDefinition PRODUCE = new SimpleAttributeDefinitionBuilder("produce", ModelType.STRING, true)
            .setStorageRuntime().build();

    public static final AttributeDefinition PRODUCES = new SimpleListAttributeDefinition.Builder("produces", PRODUCE)
            .setStorageRuntime().build();

    public static final AttributeDefinition JAVA_METHOD = new SimpleAttributeDefinitionBuilder("java-method", ModelType.STRING,
            true).setStorageRuntime().build();

    public static final ObjectTypeAttributeDefinition RESOURCE_PATH_GRP = new ObjectTypeAttributeDefinition.Builder(
            "rest-resource-path-group", RESOURCE_PATH, CONSUMES, PRODUCES, JAVA_METHOD, RESOURCE_METHODS).build();

    public static final ObjectListAttributeDefinition RESOURCE_PATHS = new ObjectListAttributeDefinition.Builder(
            "rest-resource-paths", RESOURCE_PATH_GRP).build();

    public static final ObjectTypeAttributeDefinition SUB_RESOURCE_LOCATOR = new ObjectTypeAttributeDefinition.Builder(
            "sub-resource-locator-group", RESOURCE_CLASS, RESOURCE_PATH, CONSUMES, PRODUCES, JAVA_METHOD, RESOURCE_METHODS).build();

    public static final ObjectListAttributeDefinition SUB_RESOURCE_LOCATORS = new ObjectListAttributeDefinition.Builder(
            "sub-resource-locators", SUB_RESOURCE_LOCATOR).build();

    private DeploymentRestResourcesDefintion() {
        super(PathElement.pathElement(REST_RESOURCE_NAME), JaxrsExtension.getResolver("deployment"));
    }

    @Override
    public void registerAttributes(ManagementResourceRegistration resourceRegistration) {
        resourceRegistration.registerMetric(RESOURCE_CLASS, new AbstractRestResReadHandler() {
            @Override
            void handleAttribute(String className, List<JaxrsResourceMethodDescription> methodInvokers,
                    List<JaxrsResourceLocatorDescription> locatorIncokers, Collection<String> servletMappings,
                    ModelNode response) {
                response.set(className);
            }
        });
        resourceRegistration.registerMetric(RESOURCE_PATHS, new AbstractRestResReadHandler() {
            @Override
            void handleAttribute(String className, List<JaxrsResourceMethodDescription> methodInvokers,
                    List<JaxrsResourceLocatorDescription> locatorIncokers, Collection<String> servletMappings,
                    ModelNode response) {
                for (JaxrsResourceMethodDescription methodDesc: methodInvokers) {
                    response.add(methodDesc.toModelNode());
                }
            }
        });

        resourceRegistration.registerMetric(SUB_RESOURCE_LOCATORS, new AbstractRestResReadHandler() {
            @Override
            void handleAttribute(String className, List<JaxrsResourceMethodDescription> methodInvokers,
                    List<JaxrsResourceLocatorDescription> locatorIncokers, Collection<String> servletMappings,
                    ModelNode response) {
                for (JaxrsResourceLocatorDescription methodDesc: locatorIncokers) {
                    response.add(methodDesc.toModelNode());
                }
            }
        });
    }

    abstract class AbstractRestResReadHandler implements OperationStepHandler {

        @Override
        public void execute(OperationContext context, ModelNode operation) throws OperationFailedException {
            PathAddress address = context.getCurrentAddress();
            String clsName = address.getLastElement().getValue();
            PathAddress parentAddress = address.getParent();
            final ModelNode subModel = context.readResourceFromRoot(
                    parentAddress.subAddress(0, parentAddress.size() - 1).append(SUBSYSTEM, UndertowExtension.SUBSYSTEM_NAME),
                    false).getModel();
            final String host = VIRTUAL_HOST.resolveModelAttribute(context, subModel).asString();
            final String contextPath = CONTEXT_ROOT.resolveModelAttribute(context, subModel).asString();
            final String server = SERVER.resolveModelAttribute(context, subModel).asString();

            final ServiceController<?> controller = context.getServiceRegistry(false).getService(
                    UndertowService.deploymentServiceName(server, host, contextPath));
            final UndertowDeploymentService deploymentService = (UndertowDeploymentService) controller.getService();
            Servlet resteasyServlet = null;
            Handle handle = deploymentService.getDeployment().getThreadSetupAction().setup(null);
            try {
                for (Map.Entry<String, ServletHandler> servletHandler : deploymentService.getDeployment().getServlets()
                        .getServletHandlers().entrySet()) {
                    if (HttpServletDispatcher.class.isAssignableFrom(servletHandler.getValue().getManagedServlet()
                            .getServletInfo().getServletClass())) {
                        resteasyServlet = (Servlet) servletHandler.getValue().getManagedServlet().getServlet().getInstance();
                        break;
                    }
                }
                if (resteasyServlet != null) {
                    final Collection<String> servletMappings = resteasyServlet.getServletConfig().getServletContext()
                            .getServletRegistration(resteasyServlet.getServletConfig().getServletName()).getMappings();
                    final ResourceMethodRegistry registry = (ResourceMethodRegistry) ((HttpServletDispatcher) resteasyServlet)
                            .getDispatcher().getRegistry();
                    context.addStep(new OperationStepHandler() {
                        @Override
                        public void execute(OperationContext context, ModelNode operation) throws OperationFailedException {
                            final ModelNode response = new ModelNode();
                            List<JaxrsResourceMethodDescription> resMethodInvokers = new ArrayList<>();
                            List<JaxrsResourceLocatorDescription> resLocatorInvokers = new ArrayList<>();
                            for (Map.Entry<String, List<ResourceInvoker>> resource : registry.getBounded().entrySet()) {
                                String mapping = resource.getKey();
                                List<ResourceInvoker> resouceInvokers = resource.getValue();
                                for (ResourceInvoker resourceInvoker : resouceInvokers) {
                                    if (ResourceMethodInvoker.class.isAssignableFrom(resourceInvoker.getClass())) {
                                        ResourceMethodInvoker methodInvoker = (ResourceMethodInvoker) resourceInvoker;
                                        if (methodInvoker.getResourceClass().getCanonicalName().equals(clsName)) {
                                            JaxrsResourceMethodDescription resMethodDesc = resMethodDescription(methodInvoker, contextPath, mapping, servletMappings);
                                            resMethodInvokers.add(resMethodDesc);
                                        }
                                    } else if (ResourceLocatorInvoker.class.isAssignableFrom(resourceInvoker.getClass())) {
                                        ResourceLocatorInvoker locatorInvoker = (ResourceLocatorInvoker) resourceInvoker;
                                        if (clsName.equals(locatorInvoker.getMethod().getDeclaringClass().getCanonicalName())) {
                                            ResourceClass resClass =  ResourceBuilder.locatorFromAnnotations(locatorInvoker.getMethod().getReturnType());
                                            JaxrsResourceLocatorDescription resLocatorDesc = resLocatorDescription(resClass, contextPath, mapping, servletMappings, new ArrayList<Class<?>>());
                                            resLocatorInvokers.add(resLocatorDesc);
                                        }
                                    }
                                }
                            }
                            Collections.sort(resMethodInvokers);
                            Collections.sort(resLocatorInvokers);
                            handleAttribute(clsName, resMethodInvokers, resLocatorInvokers, servletMappings, response);
                            context.getResult().set(response);
                        }
                    }, OperationContext.Stage.RUNTIME);
                }
            } catch (ServletException ex) {
                throw new RuntimeException(ex);
            } finally {
                handle.tearDown();
            }
        }

        abstract void handleAttribute(String className, List<JaxrsResourceMethodDescription> methodInvokers,
                List<JaxrsResourceLocatorDescription> locatorIncokers, Collection<String> servletMappings, ModelNode response);
    }

    private JaxrsResourceLocatorDescription resLocatorDescription(ResourceClass resClass, String contextPath, String mapping,
            Collection<String> servletMappings, List<Class<?>> resolvedCls) {
        JaxrsResourceLocatorDescription locatorRes = new JaxrsResourceLocatorDescription();
        locatorRes.resourceClass = resClass.getClazz();
        resolvedCls.add(resClass.getClazz());
        for (ResourceMethod resMethod : resClass.getResourceMethods()) {
            JaxrsResourceMethodDescription jaxrsRes = new JaxrsResourceMethodDescription();
            jaxrsRes.consumeTypes = resMethod.getConsumes();
            jaxrsRes.contextPath = contextPath;
            jaxrsRes.httpMethods = resMethod.getHttpMethods();
            jaxrsRes.method = resMethod.getMethod();
            jaxrsRes.produceTypes = resMethod.getProduces();
            jaxrsRes.resourceClass = resClass.getClazz();
            String resPath = new StringBuilder(mapping).append("/").append(resMethod.getFullpath()).toString().replace("//", "/");
            jaxrsRes.resourcePath = resPath;
            jaxrsRes.servletMappings = servletMappings;
            addMethodParameters(jaxrsRes, resMethod.getMethod());
            locatorRes.methodsDescriptions.add(jaxrsRes);
        }
        for (ResourceLocator resLocator: resClass.getResourceLocators()) {
            Class<?> clz = resLocator.getReturnType();
            if (clz.equals(resClass.getClazz())) {
                break;
            }
            if (resolvedCls.contains(clz)) {
                break;
            } else {
                resolvedCls.add(clz);
            }
            ResourceClass subResClass = ResourceBuilder.locatorFromAnnotations(clz);
            String subMapping = new StringBuilder(mapping).append("/").append(resLocator.getFullpath()).toString().replace("//", "/");
            JaxrsResourceLocatorDescription inner = resLocatorDescription(subResClass, contextPath, subMapping, servletMappings, resolvedCls);
            if (inner.containsMethodResources()) {
                locatorRes.subLocatorDescriptions.add(inner);
            }
        }
        return locatorRes;
    }

    private JaxrsResourceMethodDescription resMethodDescription(ResourceMethodInvoker methodInvoker, String contextPath,
            String mapping, Collection<String> servletMappings) {
        JaxrsResourceMethodDescription jaxrsRes = new JaxrsResourceMethodDescription();
        jaxrsRes.consumeTypes = methodInvoker.getConsumes();
        jaxrsRes.contextPath = contextPath;
        jaxrsRes.httpMethods = methodInvoker.getHttpMethods();
        jaxrsRes.method = methodInvoker.getMethod();
        jaxrsRes.produceTypes = methodInvoker.getProduces();
        jaxrsRes.resourceClass = methodInvoker.getResourceClass();
        jaxrsRes.resourcePath = mapping;
        jaxrsRes.servletMappings = servletMappings;
        addMethodParameters(jaxrsRes, methodInvoker.getMethod());
        return jaxrsRes;
    }

    private void addMethodParameters(JaxrsResourceMethodDescription jaxrsRes, Method method) {
        for (Parameter param : method.getParameters()) {
            ParamInfo paramInfo = new ParamInfo();
            paramInfo.cls = param.getType();
            paramInfo.defaultValue = null;
            paramInfo.name = null;
            paramInfo.type = null;
            Annotation annotation;
            if ((annotation = param.getAnnotation(PathParam.class)) != null) {
                PathParam pathParam = (PathParam) annotation;
                paramInfo.name = pathParam.value();
                paramInfo.type = "@" + PathParam.class.getSimpleName();
            } else if ((annotation = param.getAnnotation(QueryParam.class)) != null) {
                QueryParam queryParam = (QueryParam) annotation;
                paramInfo.name = queryParam.value();
                paramInfo.type = "@" + QueryParam.class.getSimpleName();
            } else if ((annotation = param.getAnnotation(HeaderParam.class)) != null) {
                HeaderParam headerParam = (HeaderParam) annotation;
                paramInfo.name = headerParam.value();
                paramInfo.type = "@" + HeaderParam.class.getSimpleName();
            } else if ((annotation = param.getAnnotation(CookieParam.class)) != null) {
                CookieParam cookieParam = (CookieParam) annotation;
                paramInfo.name = cookieParam.value();
                paramInfo.type = "@" + CookieParam.class.getSimpleName();
            } else if ((annotation = param.getAnnotation(MatrixParam.class)) != null) {
                MatrixParam matrixParam = (MatrixParam) annotation;
                paramInfo.name = matrixParam.value();
                paramInfo.type = "@" + MatrixParam.class.getSimpleName();
            } else if ((annotation = param.getAnnotation(FormParam.class)) != null) {
                FormParam formParam = (FormParam) annotation;
                paramInfo.name = formParam.value();
                paramInfo.type = "@" + FormParam.class.getSimpleName();
            }
            if (paramInfo.name == null) {
                paramInfo.name = param.getName();
            }
            if ((annotation = param.getAnnotation(DefaultValue.class)) != null) {
                DefaultValue defaultValue = (DefaultValue) annotation;
                paramInfo.defaultValue = defaultValue.value();
            }
            jaxrsRes.parameters.add(paramInfo);
        }
    }

    private static class JaxrsResourceLocatorDescription implements Comparable<JaxrsResourceLocatorDescription> {

        private Class<?> resourceClass;
        private List<JaxrsResourceMethodDescription> methodsDescriptions = new ArrayList<>();
        private List<JaxrsResourceLocatorDescription> subLocatorDescriptions = new ArrayList<>();

        @Override
        public int compareTo(JaxrsResourceLocatorDescription o) {
            return resourceClass.getCanonicalName().compareTo(o.resourceClass.getCanonicalName());
        }

        public ModelNode toModelNode() {
            ModelNode node = new ModelNode();
            node.get(RESOURCE_CLASS.getName()).set(resourceClass.getCanonicalName());
            ModelNode resPathNode = node.get(RESOURCE_PATHS.getName());
            Collections.sort(methodsDescriptions);
            for (JaxrsResourceMethodDescription methodRes : methodsDescriptions) {
                resPathNode.add(methodRes.toModelNode());
            }
            ModelNode subResNode = node.get(SUB_RESOURCE_LOCATORS.getName());
            Collections.sort(subLocatorDescriptions);
            for (JaxrsResourceLocatorDescription subLocator : subLocatorDescriptions) {
                subResNode.add(subLocator.toModelNode());
            }
            return node;
        }

        private boolean containsMethodResources() {
            if (this.methodsDescriptions.size() > 0) {
                return true;
            }
            return this.subLocatorDescriptions.stream().anyMatch(p -> p.containsMethodResources());
        }
    }

    private static class JaxrsResourceMethodDescription implements Comparable<JaxrsResourceMethodDescription> {

        private Class<?> resourceClass;
        private String resourcePath;
        private Method method;
        private List<ParamInfo> parameters = new ArrayList<>();
        private Set<String> httpMethods = Collections.emptySet();
        private MediaType[] consumeTypes;
        private MediaType[] produceTypes;

        private Collection<String> servletMappings = Collections.emptyList();
        private String contextPath;

        @Override
        public int compareTo(JaxrsResourceMethodDescription other) {
            int result = this.resourcePath.compareTo(other.resourcePath);
            if (result == 0) {
                result = this.resourceClass.getCanonicalName().compareTo(other.resourceClass.getCanonicalName());
            }
            if (result == 0) {
                result = this.method.getName().compareTo(other.method.getName());
            }
            return result;
        }

        ModelNode toModelNode() {
            ModelNode node = new ModelNode();
            node.get(RESOURCE_PATH.getName()).set(resourcePath);
            ModelNode consumeNode = node.get(CONSUMES.getName());
            if (consumeTypes != null && consumeTypes.length > 0) {
                for (MediaType consume : consumeTypes) {
                    consumeNode.add(consume.toString());
                }
            }
            ModelNode produceNode = node.get(PRODUCES.getName());
            if (produceTypes != null && produceTypes.length > 0) {
                for (MediaType produce : produceTypes) {
                    produceNode.add(produce.toString());
                }
            }
            node.get(JAVA_METHOD.getName()).set(formatJavaMethod());
            for (final String servletMapping : servletMappings) {
                for (final String httpMethod : httpMethods) {
                    node.get(RESOURCE_METHODS.getName()).add(httpMethod + " " + formatPath(servletMapping, contextPath, resourcePath));
                }
            }
            return node;
        }

        private String formatPath(String servletMapping, String ctxPath, String resPath) {
            StringBuilder sb = new StringBuilder();
            String servletPath = servletMapping.replaceAll("\\*", "");
            if (servletPath.charAt(0) == '/') {
                servletPath = servletPath.substring(1);
            }
            sb.append(ctxPath).append('/').append(servletPath).append(resPath);
            return sb.toString().replace("//", "/");
        }

        private String formatJavaMethod() {
            StringBuilder sb = new StringBuilder();
            sb.append(method.getReturnType().getCanonicalName()).append(" ").append(resourceClass.getCanonicalName())
                    .append(".").append(method.getName()).append('(');
            int i = 0;
            for (ParamInfo param : this.parameters) {
                if (param.type != null) {
                    sb.append(param.type).append(" ");
                }
                sb.append(param.cls.getCanonicalName()).append(" ").append(param.name);
                if (param.defaultValue != null) {
                    sb.append(" = '");
                    sb.append(param.defaultValue);
                    sb.append("'");
                }
                if (++i < this.parameters.size()) {
                    sb.append(", ");
                }
            }
            sb.append(")");
            return sb.toString();
        }
    }

    private static class ParamInfo {
        private String name;
        private Class<?> cls;
        private String type; // @PathParam, or @CookieParam, or @QueryParam, etc
        private String defaultValue;
    }
}
