package org.jboss.resteasy.core;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jakarta.ws.rs.Path;

import org.jboss.resteasy.core.registry.RootClassNode;
import org.jboss.resteasy.core.registry.RootNode;
import org.jboss.resteasy.plugins.server.resourcefactory.JndiResourceFactory;
import org.jboss.resteasy.plugins.server.resourcefactory.POJOResourceFactory;
import org.jboss.resteasy.plugins.server.resourcefactory.SingletonResource;
import org.jboss.resteasy.resteasy_jaxrs.i18n.LogMessages;
import org.jboss.resteasy.resteasy_jaxrs.i18n.Messages;
import org.jboss.resteasy.specimpl.ResteasyUriBuilderImpl;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.InjectorFactory;
import org.jboss.resteasy.spi.Registry;
import org.jboss.resteasy.spi.ResourceFactory;
import org.jboss.resteasy.spi.ResourceInvoker;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.spi.ResteasyUriBuilder;
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.jboss.resteasy.spi.statistics.StatisticsController;
import org.jboss.resteasy.tracing.RESTEasyTracingLogger;
import org.jboss.resteasy.util.AnnotationResolver;
import org.jboss.resteasy.util.GetRestful;
import org.jboss.resteasy.util.IsHttpMethod;

/**
 * Registry of resources and methods/classes that can dispatch HTTP method requests.
 *
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
@SuppressWarnings("rawtypes")
public class ResourceMethodRegistry implements Registry {
    public static final String REGISTRY_MATCHING_EXCEPTION = "registry.matching.exception";

    protected ResteasyProviderFactory providerFactory;
    protected RootClassNode root = new RootClassNode();
    protected boolean widerMatching;
    protected RootNode rootNode = new RootNode();
    protected ResourceBuilder resourceBuilder;
    protected StatisticsController statisticsController;

    public ResourceMethodRegistry(final ResteasyProviderFactory providerFactory) {
        this.providerFactory = providerFactory;
        this.resourceBuilder = providerFactory.getResourceBuilder();
        this.statisticsController = providerFactory.getStatisticsController();
    }

    public boolean isWiderMatching() {
        return widerMatching;
    }

    public void setWiderMatching(boolean widerMatching) {
        this.widerMatching = widerMatching;
    }

    public void addPerRequestResource(Class clazz, String basePath) {
        addResourceFactory(new POJOResourceFactory(resourceBuilder, clazz), basePath);

    }

    /**
     * Register a vanilla JAX-RS resource class.
     *
     * @param clazz class
     */
    public void addPerRequestResource(Class clazz) {
        addResourceFactory(new POJOResourceFactory(resourceBuilder, clazz));
    }

    @Override
    public void addPerRequestResource(Class<?> clazz, ResourceBuilder resourceBuilder) {
        addResourceFactory(new POJOResourceFactory(resourceBuilder, clazz), resourceBuilder);
    }

    @Override
    public void addPerRequestResource(ResourceClass clazz) {
        POJOResourceFactory resourceFactory = new POJOResourceFactory(resourceBuilder, clazz);
        register(resourceFactory, null, clazz);
        resourceFactory.registered(providerFactory);
    }

    @Override
    public void addPerRequestResource(ResourceClass clazz, String basePath) {
        POJOResourceFactory resourceFactory = new POJOResourceFactory(resourceBuilder, clazz);
        register(resourceFactory, basePath, clazz);
        resourceFactory.registered(providerFactory);
    }

    public void addSingletonResource(Object singleton) {
        ResourceClass resourceClass = resourceBuilder.getRootResourceFromAnnotations(singleton.getClass());
        addResourceFactory(new SingletonResource(singleton, resourceClass));
    }

    public void addSingletonResource(Object singleton, String basePath) {
        ResourceClass resourceClass = resourceBuilder.getRootResourceFromAnnotations(singleton.getClass());
        addResourceFactory(new SingletonResource(singleton, resourceClass), basePath);
    }

    @Override
    public void addSingletonResource(Object singleton, ResourceClass resourceClass) {
        SingletonResource resourceFactory = new SingletonResource(singleton, resourceClass);
        register(resourceFactory, null, resourceClass);
        resourceFactory.registered(providerFactory);
    }

    @Override
    public void addSingletonResource(Object singleton, ResourceClass resourceClass, String basePath) {
        SingletonResource resourceFactory = new SingletonResource(singleton, resourceClass);
        register(resourceFactory, basePath, resourceClass);
        resourceFactory.registered(providerFactory);
    }

    public void addJndiResource(String jndiName) {
        addResourceFactory(new JndiResourceFactory(jndiName));
    }

    public void addJndiResource(String jndiName, String basePath) {
        addResourceFactory(new JndiResourceFactory(jndiName), basePath);
    }

    @Override
    public void addJndiResource(String jndiName, ResourceClass resourceClass) {
        JndiResourceFactory resourceFactory = new JndiResourceFactory(jndiName);
        register(resourceFactory, null, resourceClass);
        resourceFactory.registered(providerFactory);
    }

    @Override
    public void addJndiResource(String jndiName, ResourceClass resourceClass, String basePath) {
        JndiResourceFactory resourceFactory = new JndiResourceFactory(jndiName);
        register(resourceFactory, basePath, resourceClass);
        resourceFactory.registered(providerFactory);
    }

    /**
     * Bind an endpoint ResourceFactory. ResourceFactory.getScannableClass() defines what class should be scanned
     * for JAX-RS annotations. The class and any implemented interfaces are scanned for annotations.
     *
     * @param ref resource factory
     */
    public void addResourceFactory(ResourceFactory ref) {
        addResourceFactory(ref, (String) null);
    }

    public void addResourceFactory(ResourceFactory ref, ResourceBuilder resourceBuilder) {
        addResourceFactory(ref, resourceBuilder, null);
    }

    /**
     * ResourceFactory.getScannableClass() defines what class should be scanned
     * for JAX-RS annotations. The class and any implemented interfaces are scanned for annotations.
     *
     * @param ref  resource factory
     * @param base base URI path for any resources provided by the factory, in addition to rootPath
     */
    public void addResourceFactory(ResourceFactory ref, String base) {
        addResourceFactory(ref, resourceBuilder, base);
    }

    public void addResourceFactory(ResourceFactory ref, ResourceBuilder resourceBuilder, String base) {
        Class<?> clazz = ref.getScannableClass();
        Class restful = AnnotationResolver.getClassWithAnnotation(clazz, resourceBuilder.getCorrespondingRootAnnotation());
        if (restful == null) {
            String msg = Messages.MESSAGES.classIsNotRootResource(clazz.getName());
            for (Class intf : clazz.getInterfaces()) {
                msg += " " + intf.getName();
            }
            throw new RuntimeException(msg);
        }
        addResourceFactory(ref, resourceBuilder, base, restful);
    }

    /**
     * ResourceFactory.getScannableClass() is not used, only the clazz parameter and not any implemented interfaces
     * of the clazz parameter.
     *
     * @param ref   resource factory
     * @param base  base URI path for any resources provided by the factory, in addition to rootPath
     * @param clazz specific class
     */
    public void addResourceFactory(ResourceFactory ref, String base, Class<?> clazz) {
        addResourceFactory(ref, resourceBuilder, base, clazz);
    }

    public void addResourceFactory(ResourceFactory ref, ResourceBuilder resourceBuilder, String base, Class<?> clazz) {
        Class<?>[] classes = { clazz };
        addResourceFactory(ref, resourceBuilder, base, classes);
    }

    /**
     * ResourceFactory.getScannableClass() is not used, only the clazz parameter and not any implemented interfaces
     * of the clazz parameter.
     *
     * @param ref     resource factory
     * @param base    base URI path for any resources provided by the factory, in addition to rootPath
     * @param classes specific class
     */
    public void addResourceFactory(ResourceFactory ref, String base, Class<?>[] classes) {
        addResourceFactory(ref, resourceBuilder, base, classes);
    }

    public void addResourceFactory(ResourceFactory ref, ResourceBuilder resourceBuilder, String base, Class<?>[] classes) {
        if (ref != null)
            ref.registered(providerFactory);
        for (Class<?> clazz : classes) {
            if (Proxy.isProxyClass(clazz)) {
                for (Class<?> intf : clazz.getInterfaces()) {
                    ResourceClass resourceClass = resourceBuilder.getRootResourceFromAnnotations(intf);
                    register(ref, base, resourceClass);
                }
            } else {
                ResourceClass resourceClass = resourceBuilder.getRootResourceFromAnnotations(clazz);
                register(ref, base, resourceClass);
            }
        }

        // https://issues.jboss.org/browse/JBPAPP-7871
        for (Class<?> clazz : classes) {
            for (Method method : clazz.getDeclaredMethods()) {
                Method _method = resourceBuilder.getAnnotatedMethod(clazz, method);
                if (_method != null && !java.lang.reflect.Modifier.isPublic(_method.getModifiers())) {
                    LogMessages.LOGGER.JAXRSAnnotationsFoundAtNonPublicMethod(method.getDeclaringClass().getName(),
                            method.getName());
                }
            }
        }

    }

    @Override
    public void addResourceFactory(ResourceFactory rf, String base, ResourceClass resourceClass) {
        if (rf != null)
            rf.registered(providerFactory);
        register(rf, base, resourceClass);
    }

    protected void register(ResourceFactory rf, String base, ResourceClass resourceClass) {
        for (ResourceMethod method : resourceClass.getResourceMethods()) {
            processMethod(rf, base, method);
        }
        for (ResourceLocator method : resourceClass.getResourceLocators()) {
            processMethod(rf, base, method);
        }
    }

    /**
     * Resteasy 2.x does not properly handle sub-resource and sub-resource locator
     * endpoints with the same uri. Resteasy 3.x does handle this properly. In
     * assisting customers identify this issue during an upgrade from Resteasy 2 to 3
     * provides a waring when the situation is found.
     */
    public void checkAmbiguousUri() {
        for (Map.Entry<String, List<ResourceInvoker>> entry : this.root.getBounded().entrySet()) {
            List<ResourceInvoker> values = entry.getValue();
            if (values.size() > 1) {
                int locatorCnt = 0;
                int methodCnt = 0;
                for (ResourceInvoker rInvoker : values) {
                    if (rInvoker instanceof ResourceLocatorInvoker) {
                        locatorCnt++;
                    } else if (rInvoker instanceof ResourceMethodInvoker) {
                        methodCnt++;
                    }
                }
                if (methodCnt > 0 && locatorCnt > 0) {
                    StringBuilder sb = new StringBuilder();
                    int cnt = values.size();
                    for (int i = 0; i < cnt; i++) {
                        ResourceInvoker exp = values.get(i);
                        sb.append(exp.getMethod().getDeclaringClass().getName())
                                .append(".")
                                .append(exp.getMethod().getName());
                        if (i < cnt - 1) {
                            sb.append(", ");
                        }
                    }
                    LogMessages.LOGGER.uriAmbiguity(entry.getKey(), sb.toString());
                }
            }
        }
    }

    protected void processMethod(ResourceFactory rf, String base, ResourceLocator method) {
        ResteasyUriBuilder builder = new ResteasyUriBuilderImpl();
        if (base != null)
            builder.path(base);
        builder.path(method.getFullpath());
        String fullpath = builder.getPath();
        if (fullpath == null)
            fullpath = "";

        builder = new ResteasyUriBuilderImpl();
        if (base != null)
            builder.path(base);
        builder.path(method.getResourceClass().getPath());
        String classExpression = builder.getPath();
        if (classExpression == null)
            classExpression = "";

        InjectorFactory injectorFactory = providerFactory.getInjectorFactory();
        if (method instanceof ResourceMethod) {
            ResourceMethodInvoker invoker = new ResourceMethodInvoker((ResourceMethod) method, injectorFactory, rf,
                    providerFactory);
            if (widerMatching)
                rootNode.addInvoker(fullpath, invoker);
            else
                root.addInvoker(classExpression, fullpath, invoker);
            statisticsController.register(invoker);
        } else {
            ResourceLocatorInvoker locator = new ResourceLocatorInvoker(rf, injectorFactory, providerFactory, method);
            if (widerMatching)
                rootNode.addInvoker(fullpath, locator);
            else
                root.addInvoker(classExpression, fullpath, locator);
            statisticsController.register(locator);
        }
    }

    /**
     * Find all endpoints reachable by clazz and unregister them.
     *
     * @param clazz class
     */
    public void removeRegistrations(Class clazz) {
        removeRegistrations(clazz, null);
    }

    public void removeRegistrations(Class clazz, String base) {
        Class restful = GetRestful.getRootResourceClass(clazz);
        removeRegistration(base, restful);
    }

    @Override
    public void removeRegistrations(ResourceClass resourceClass) {
        for (ResourceMethod method : resourceClass.getResourceMethods()) {
            if (widerMatching)
                rootNode.removeBinding(method.getFullpath(), method.getMethod());
            else
                root.removeBinding(resourceClass.getPath(), method.getFullpath(), method.getMethod());
        }
        for (ResourceLocator method : resourceClass.getResourceLocators()) {
            if (widerMatching)
                rootNode.removeBinding(method.getFullpath(), method.getMethod());
            else
                root.removeBinding(resourceClass.getPath(), method.getFullpath(), method.getMethod());
        }
    }

    private void removeRegistration(String base, Class<?> clazz) {
        for (Method method : clazz.getMethods()) {
            Path path = method.getAnnotation(Path.class);
            Set<String> httpMethods = IsHttpMethod.getHttpMethods(method);
            if (path == null && httpMethods == null)
                continue;

            ResteasyUriBuilder builder = new ResteasyUriBuilderImpl();
            if (base != null)
                builder.path(base);
            if (clazz.isAnnotationPresent(Path.class))
                builder.path(clazz);
            String classExpression = builder.getPath();
            if (path != null)
                builder.path(method);
            String fullpath = builder.getPath();
            if (fullpath == null)
                fullpath = "";

            if (widerMatching)
                rootNode.removeBinding(fullpath, method);
            else
                root.removeBinding(classExpression, fullpath, method);
        }
    }

    public Map<String, List<ResourceInvoker>> getBounded() {
        if (widerMatching)
            return rootNode.getBounded();
        else
            return root.getBounded();
    }

    /**
     * Number of endpoints registered.
     *
     * @return number of endpoints registered
     */
    public int getSize() {
        if (widerMatching)
            return rootNode.getSize();
        else
            return root.getSize();
    }

    /**
     * Find a resource to invoke on.
     *
     * @return resource invoker
     */
    public ResourceInvoker getResourceInvoker(HttpRequest request) {
        RESTEasyTracingLogger tracingLogger = RESTEasyTracingLogger.getInstance(request);
        final long timestamp = tracingLogger.timestamp("MATCH_SUMMARY");
        try {
            if (widerMatching) {
                return rootNode.match(request, 0);
            } else {
                return root.match(request, 0);
            }
        } catch (RuntimeException e) {
            throw e;
        } finally {
            tracingLogger.logDuration("MATCH_SUMMARY", timestamp);
        }
    }
}
