package org.jboss.resteasy.jsapi;

import org.jboss.resteasy.annotations.Form;
import org.jboss.resteasy.core.ResourceInvoker;
import org.jboss.resteasy.core.ResourceMethodInvoker;
import org.jboss.resteasy.jsapi.MethodParamMetaData.MethodParamType;
import org.jboss.resteasy.jsapi.i18n.LogMessages;
import org.jboss.resteasy.jsapi.i18n.Messages;
import org.jboss.resteasy.spi.metadata.ResourceLocator;
import org.jboss.resteasy.spi.metadata.ResourceMethod;
import org.jboss.resteasy.util.FindAnnotation;

import javax.ws.rs.Consumes;
import javax.ws.rs.CookieParam;
import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.MatrixParam;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

public class MethodMetaData
{

   private ResourceMethodInvoker invoker;
   private Method method;
   private ResourceMethod resourceMethod;
   private Class<?> klass;
   private String wants;
   private String consumesMIMEType;
   private String uri;
   private String functionName;
   private List<MethodParamMetaData> parameters = new ArrayList<MethodParamMetaData>();
   private Collection<String> httpMethods;
   private ServiceRegistry registry;
   private String functionPrefix;
   private boolean wantsForm;

   public MethodMetaData(ServiceRegistry serviceRegistry, ResourceMethodInvoker invoker) throws Exception {
      this.registry = serviceRegistry;
      this.invoker = invoker;
      this.method = invoker.getMethod();

      this.resourceMethod = (ResourceMethod) getResourceLocator(invoker);
      this.klass = invoker.getResourceClass();

      String methodPath = resourceMethod.getPath();
      String klassPath = resourceMethod.getResourceClass().getPath();
      Produces produces = method.getAnnotation(Produces.class);

      if (produces == null)
         produces = klass.getAnnotation(Produces.class);
      this.wants = getWants(produces);
      Consumes consumes = method.getAnnotation(Consumes.class);
      if (consumes == null)
         consumes = klass.getAnnotation(Consumes.class);


      this.uri = appendURIFragments(registry, klassPath, methodPath);

      if(serviceRegistry.isRoot())
         this.functionPrefix = klass.getSimpleName();
      else
         this.functionPrefix = serviceRegistry.getFunctionPrefix();
      this.functionName = this.functionPrefix + "." + method.getName();
      httpMethods = invoker.getHttpMethods();

      // we need to add all parameters from parent resource locators until the root
      List<Method> methodsUntilRoot = new ArrayList<Method>();
      methodsUntilRoot.add(method);
      serviceRegistry.collectResourceMethodsUntilRoot(methodsUntilRoot);
      for (Method method : methodsUntilRoot) {
         Annotation[][] allAnnotations = method.getParameterAnnotations();
         Class<?>[] parameterTypes = method.getParameterTypes();
         for (int i = 0; i < parameterTypes.length; i++)
         {
            processMetaData(parameterTypes[i], allAnnotations[i], true);
         }
      }
      // this must be after we scan the params in case of @Form
      this.consumesMIMEType = getConsumes(consumes);
      if(wantsForm && !"application/x-www-form-urlencoded".equals(consumesMIMEType)){
         LogMessages.LOGGER.warn(Messages.MESSAGES.overridingConsumes());
         this.consumesMIMEType = "application/x-www-form-urlencoded";
      }
   }

   public static ResourceLocator getResourceLocator(ResourceInvoker invoker) throws Exception {
      Field resourceMethodField = null;
      resourceMethodField = invoker.getClass().getDeclaredField("method");
      resourceMethodField.setAccessible(true);
      return (ResourceLocator) resourceMethodField.get(invoker);
   }

   protected void processMetaData(Class<?> type, Annotation[] annotations,
         boolean useBody)
   {
      QueryParam query;
      HeaderParam header;
      MatrixParam matrix;
      PathParam uriParam;
      CookieParam cookie;
      FormParam formParam;
      Form form;

      // boolean isEncoded = FindAnnotation.findAnnotation(annotations,
      // Encoded.class) != null;

      if ((query = FindAnnotation.findAnnotation(annotations, QueryParam.class)) != null)
      {
         addParameter(type, annotations, MethodParamType.QUERY_PARAMETER, query
               .value());
      } else if ((header = FindAnnotation.findAnnotation(annotations,
            HeaderParam.class)) != null)
      {
         addParameter(type, annotations, MethodParamType.HEADER_PARAMETER,
               header.value());
      } else if ((cookie = FindAnnotation.findAnnotation(annotations,
            CookieParam.class)) != null)
      {
         addParameter(type, annotations, MethodParamType.COOKIE_PARAMETER,
               cookie.value());
      } else if ((uriParam = FindAnnotation.findAnnotation(annotations,
            PathParam.class)) != null)
      {
         addParameter(type, annotations, MethodParamType.PATH_PARAMETER,
               uriParam.value());
      } else if ((matrix = FindAnnotation.findAnnotation(annotations,
            MatrixParam.class)) != null)
      {
         addParameter(type, annotations, MethodParamType.MATRIX_PARAMETER,
               matrix.value());
      } else if ((formParam = FindAnnotation.findAnnotation(annotations,
            FormParam.class)) != null)
      {
         addParameter(type, annotations, MethodParamType.FORM_PARAMETER,
               formParam.value());
         this.wantsForm = true;
      } else if ((form = FindAnnotation.findAnnotation(annotations, Form.class)) != null)
      {
         if (type == Map.class || type == List.class) {
            addParameter(type, annotations, MethodParamType.FORM, form.prefix());
            this.wantsForm = true;
         } else
            walkForm(type);
      } else if ((FindAnnotation.findAnnotation(annotations, Context.class)) != null)
      {
         // righfully ignore
      } else if (useBody)
      {
         addParameter(type, annotations, MethodParamType.ENTITY_PARAMETER, null);
      }
   }

   private void walkForm(Class<?> type)
   {
      for (Field field : type.getDeclaredFields())
      {
         processMetaData(field.getType(), field.getAnnotations(), false);
      }
      for (Method method : type.getDeclaredMethods())
      {
         if (method.getParameterTypes().length != 1
               || !method.getReturnType().equals(Void.class))
            continue;
         processMetaData(method.getParameterTypes()[0],
               method.getAnnotations(), false);
      }
      if (type.getSuperclass() != null) {
         walkForm(type.getSuperclass());
      }
   }

   private void addParameter(Class<?> type, Annotation[] annotations,
         MethodParamType paramType, String value)
   {
      this.parameters.add(new MethodParamMetaData(type, annotations, paramType,
            value));
   }

   private String getWants(Produces produces)
   {
      if (produces == null)
         return null;
      String[] value = produces.value();
      if (value.length == 0)
         return null;
      if (value.length == 1)
         return value[0];
      StringBuffer buf = new StringBuffer();
      for (String mime : produces.value())
      {
         if (buf.length() != 0)
            buf.append(",");
         buf.append(mime);
      }
      return buf.toString();
   }

   private String getConsumes(Consumes consumes)
   {
      if (consumes == null)
         return "text/plain";
      if (consumes.value().length > 0)
         return consumes.value()[0];
      return "text/plain";
   }

   public static String appendURIFragments(ServiceRegistry registry, String classPath, String methodPath) {
      return appendURIFragments(registry == null ? null : registry.getUri(),
            notEmpty(classPath) ? classPath : null,
            notEmpty(methodPath) ? methodPath : null);
   }

   public static String appendURIFragments(String... fragments)
   {
      StringBuilder str = new StringBuilder();
      for (String fragment : fragments)
      {
         if (fragment == null || fragment.length() == 0 || fragment.equals("/"))
            continue;
         if (fragment.startsWith("/"))
            fragment = fragment.substring(1);
         if (fragment.endsWith("/"))
            fragment = fragment.substring(0, fragment.length() - 1);
         str.append('/').append(fragment);
      }
      if (str.length() == 0)
         return "/";
      return str.toString();
   }

   public ResourceMethodInvoker getInvoker()
   {
      return invoker;
   }

   public Method getMethod()
   {
      return method;
   }

   public Class<?> getKlass()
   {
      return klass;
   }

   public String getWants()
   {
      return wants;
   }

   public String getConsumesMIMEType()
   {
      return consumesMIMEType;
   }

   public String getUri()
   {
      return uri;
   }

   public String getFunctionName()
   {
      return functionName;
   }

   public List<MethodParamMetaData> getParameters()
   {
      return parameters;
   }

   public Collection<String> getHttpMethods()
   {
      return httpMethods;
   }


   private static boolean notEmpty(String string) {
      return string != null && !string.isEmpty();
   }

   public String getFunctionPrefix() {
      return functionPrefix;
   }
}
