package org.jboss.resteasy.links.impl;

import org.jboss.resteasy.annotations.Form;
import org.jboss.resteasy.core.ResourceInvoker;
import org.jboss.resteasy.core.ResourceMethodInvoker;
import org.jboss.resteasy.core.ResourceMethodRegistry;
import org.jboss.resteasy.links.ELProvider;
import org.jboss.resteasy.links.LinkELProvider;
import org.jboss.resteasy.links.LinkResource;
import org.jboss.resteasy.links.LinkResources;
import org.jboss.resteasy.links.ParamBinding;
import org.jboss.resteasy.links.RESTServiceDiscovery;
import org.jboss.resteasy.links.ResourceFacade;
import org.jboss.resteasy.links.i18n.LogMessages;
import org.jboss.resteasy.links.i18n.Messages;
import org.jboss.resteasy.specimpl.ResteasyUriBuilder;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.util.FindAnnotation;

import javax.annotation.security.RolesAllowed;
import javax.el.ELContext;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

public class RESTUtils {

   public static <T> T addDiscovery(T entity, UriInfo uriInfo, ResourceMethodRegistry registry) {
      // find the field to inject first
      Field injectionField = findInjectionField(entity);
      if (injectionField == null)
         return entity;
      List<Method> methods = getServiceMethods(registry);

      RESTServiceDiscovery ret = null;
      try {
         injectionField.setAccessible(true);
         ret = (RESTServiceDiscovery) injectionField.get(entity);
      } catch (Exception e) {
         LogMessages.LOGGER.error(Messages.MESSAGES.failedToReuseServiceDiscovery(entity), e);
      }
      if (ret == null) {
         ret = new RESTServiceDiscovery();
      }
      for(Method m : methods){
         processLinkResources(m, entity, uriInfo, ret);
      }
      // do not inject an empty service
      if(ret.isEmpty())
         return entity;
      // now inject
      try {
         injectionField.set(entity, ret);
         injectionField.setAccessible(false);
      } catch (Exception e) {
         LogMessages.LOGGER.error(Messages.MESSAGES.failedToInjectLinks(entity), e);
      }
      return entity;
   }

   private static Field findInjectionField(Object entity) {
      Class<?> klass = entity.getClass();
      do{
         for(Field f : klass.getDeclaredFields()){
            if(f.getType().equals(RESTServiceDiscovery.class))
               return f;
         }
         klass = klass.getSuperclass();
      }while(klass != null);
      return null;
   }

   public static List<Method> getServiceMethods(ResourceMethodRegistry registry){
      ArrayList<Method> results = new ArrayList<Method>();
      for (Entry<String, List<ResourceInvoker>> entry : registry.getBounded().entrySet())
      {
         List<ResourceInvoker> invokers = entry.getValue();
         for (ResourceInvoker invoker : invokers)
         {
            if (invoker instanceof ResourceMethodInvoker)
            {
               ResourceMethodInvoker resourceMethod = (ResourceMethodInvoker)invoker;
               Method method = resourceMethod.getMethod();
               results.add(method);
            } else
            {
               // TODO: fix this?
            }
         }
      }
      return results;
   }

   private static void processLinkResources(Method m, Object entity, UriInfo uriInfo,
         RESTServiceDiscovery ret) {
      // find a single service
      LinkResource service = m
         .getAnnotation(LinkResource.class);
      if(service != null)
         processLinkResource(m, entity, uriInfo, ret, service);
      // find a multi-service
      LinkResources services = m
         .getAnnotation(LinkResources.class);
      if(services != null)
         for(LinkResource service2 : services.value())
            processLinkResource(m, entity, uriInfo, ret, service2);
   }

   private static void processLinkResource(Method m, Object entity, UriInfo uriInfo,
         RESTServiceDiscovery ret, LinkResource service) {
      String rel = service.rel();
      // if we have uri templates, we need a compatible instance
      Class<?> type = getServiceType(service, m);
      if(type.isInstance(entity)){
         if(checkConstraint(service, entity, m))
            addInstanceService(m, entity, uriInfo, ret, service, rel);
      }else if(entity instanceof ResourceFacade<?> && ((ResourceFacade<?>)entity).facadeFor() == type){
         if(checkConstraint(service, type, m))
            addService(m, (ResourceFacade<?>) entity, uriInfo, ret, service, rel);
      }
   }

   private static Class<?> getServiceType(LinkResource service, Method m) {
      Class<?> type = service.value();
      if(type != Void.class)
         return type;
      // are we looking at the return type or the body type?
      type = findBodyType(m);
      if(type == null){
         // our next best bet is the return type
         type = m.getReturnType();
      }
      if(Void.TYPE == type)
         throw new ServiceDiscoveryException(m, Messages.MESSAGES.cannotGuessResourceType());
      if(Collection.class.isAssignableFrom(type))
         throw new ServiceDiscoveryException(m, Messages.MESSAGES.cannotGuessCollectionType());
      if(Response.class.isAssignableFrom(type))
         throw new ServiceDiscoveryException(m, Messages.MESSAGES.cannotGuessType());
      return type;
   }

   private static Class<?> findBodyType(Method m) {
      Annotation[][] annotations = m.getParameterAnnotations();
      Class<?>[] types = m.getParameterTypes();
      for (int i = 0; i < types.length; i++) {
         // if there's no JAXRS annotation nor @Form, it's a body right?
         if(FindAnnotation.findJaxRSAnnotations(annotations[i]).length == 0
               && FindAnnotation.findAnnotation(annotations[i], Form.class) == null)
            return types[i];
      }
      return null;
   }

   private static boolean checkConstraint(LinkResource service, Object object, Method m) {
      String constraint = service.constraint();
      if(constraint == null || constraint.length() == 0)
         return checkEJBConstraint(m);
      Boolean ret = evaluateELBoolean(m, getELContext(m, object), object, constraint);
      return ret != null && ret.booleanValue();
   }

   private static boolean checkEJBConstraint(Method m) {
      // Use dynamic class loading here since if the EJB annotation class is not present
      // it cannot be on the method, so we don't have to check for it
      try {
         Class.forName("javax.annotation.security.RolesAllowed");
      } catch (ClassNotFoundException e) {
         // class not here, therefore not on method either
         return true;
      }
      // From now on we can use this class since it's there. I (Stef Epardaud) don't think we need to
      // remove the reference here and use reflection.
      RolesAllowed rolesAllowed = m.getAnnotation(RolesAllowed.class);
      if(rolesAllowed == null)
         return true;
      SecurityContext context = ResteasyProviderFactory.getContextData(SecurityContext.class);
      for(String role : rolesAllowed.value())
         if(context.isUserInRole(role))
            return true;
      return false;
   }

   private static void addService(Method m, ResourceFacade<?> entity, UriInfo uriInfo,
         RESTServiceDiscovery ret, LinkResource service, String rel) {
      Map<String, ? extends Object> pathParameters = entity.pathParameters();
      // do we need any path parameters?
      UriBuilder uriBuilder = uriInfo.getBaseUriBuilder().path(m.getDeclaringClass());
      if(m.isAnnotationPresent(Path.class))
         uriBuilder.path(m);
      URI uri;
      List<String> paramNames = ((ResteasyUriBuilder)uriBuilder).getPathParamNamesInDeclarationOrder();
      if(paramNames.isEmpty())
         uri = uriBuilder.build();
      else if(pathParameters.size() >= paramNames.size())
         uri = uriBuilder.buildFromMap(pathParameters);
      else
         // just bail out since we don't have enough parameters, that must be an instance service
         return;
      if(rel.length() == 0){
         if (m.isAnnotationPresent(GET.class))
            rel = "list";
         else if (m.isAnnotationPresent(POST.class))
            rel = "add";
      }
      ret.addLink(uri, rel);
   }

   private static void addInstanceService(Method m, Object entity,
         UriInfo uriInfo, RESTServiceDiscovery ret, LinkResource service,
         String rel) {
      UriBuilder uriBuilder = uriInfo.getBaseUriBuilder().path(m.getDeclaringClass());
      if(m.isAnnotationPresent(Path.class))
         uriBuilder.path(m);
      URI uri = buildURI(uriBuilder, service, entity, m);

      if (rel.length() == 0) {
         if (m.isAnnotationPresent(GET.class)){
            Class<?> type = m.getReturnType();
            if(Collection.class.isAssignableFrom(type))
               rel = "list";
            else
               rel = "self";
         }else if (m.isAnnotationPresent(PUT.class))
            rel = "update";
         else if (m.isAnnotationPresent(POST.class))
            rel = "add";
         else if (m.isAnnotationPresent(DELETE.class))
            rel = "remove";
      }
      ret.addLink(uri, rel);
   }

   private static URI buildURI(UriBuilder uriBuilder, LinkResource service,
         Object entity, Method m) {
      for (ParamBinding binding : service.queryParameters()) {
         uriBuilder.queryParam(binding.name(), evaluateEL(m, getELContext(m, entity), entity, binding.value()));
      }
      for (ParamBinding binding : service.matrixParameters()) {
         uriBuilder.matrixParam(binding.name(), evaluateEL(m, getELContext(m, entity), entity, binding.value()));
      }

      String[] uriTemplates = service.pathParameters();
      if (uriTemplates.length > 0) {
         Object[] values = new Object[uriTemplates.length];
         for (int i = 0; i < values.length; i++)
            values[i] = evaluateEL(m, getELContext(m, entity), entity, uriTemplates[i]);
         return uriBuilder.build(values);
      }
      // do we need any path parameters?
      List<String> paramNames = ((ResteasyUriBuilder)uriBuilder).getPathParamNamesInDeclarationOrder();
      if(paramNames.isEmpty())
         return uriBuilder.build();
      // try to find the IDs
      List<Object> params = findURIParamsFromResource(entity);
      if(params.size() == paramNames.size())
         return uriBuilder.build(params.toArray());
      // if we have too many, ignore the last ones
      if(params.size() > paramNames.size())
         return uriBuilder.build(params.subList(0, paramNames.size()).toArray());
      throw new ServiceDiscoveryException(m, Messages.MESSAGES.notEnoughtUriParameters(paramNames.size(), params.size()));
   }

   private static List<Object> findURIParamsFromResource(Object entity) {
      List<Object> ids = new ArrayList<Object>();
      do{
         List<Object> theseIDs = BeanUtils.findIDs(entity);
         ids.addAll(0, theseIDs);
      }while((entity = BeanUtils.findParentResource(entity)) != null);
      return ids;
   }

   private static LinkELProvider findLinkELProvider(Method m){
      if(m.isAnnotationPresent(LinkELProvider.class))
         return m.getAnnotation(LinkELProvider.class);
      Class<?> c = m.getDeclaringClass();
      if(c.isAnnotationPresent(LinkELProvider.class))
         return c.getAnnotation(LinkELProvider.class);
      Package p = c.getPackage();
      if(p != null && p.isAnnotationPresent(LinkELProvider.class))
         return p.getAnnotation(LinkELProvider.class);
      return null;
   }

   private static ELProvider getELProvider(Method m){
      LinkELProvider linkElProvider = findLinkELProvider(m);
      if(linkElProvider == null)
         return null;
      Class<? extends ELProvider> elProviderClass = linkElProvider.value();
      try{
         return elProviderClass.newInstance();
      }catch(Exception x){
         LogMessages.LOGGER.error(Messages.MESSAGES.couldNotInstantiateELProviderClass(elProviderClass.getName()), x);
         throw new ServiceDiscoveryException(m, Messages.MESSAGES.failedToInstantiateELProvider(elProviderClass.getName()), x);
      }
   }

   private static ELContext getELContext(Method m, Object base){
      ELContext ours = EL.createELContext(base);
      ELProvider elProvider = getELProvider(m);
      if(elProvider != null)
         return elProvider.getContext(ours);
      return ours;
   }

   public static Map<String, ? extends Object> derivePathParameters(UriInfo uriInfo){
      MultivaluedMap<String, String> pathParameters = uriInfo.getPathParameters();
      Map<String, String> ret = new HashMap<String,String>();
      for(Entry<String, List<String>> entry:  pathParameters.entrySet()){
         ret.put(entry.getKey(), entry.getValue().get(0));
      }
      return ret;
   }

   public static Object evaluateEL(Method m, ELContext context, Object base, String expression) {
      try{
         return EL.EXPRESSION_FACTORY.createValueExpression(context, expression,
               Object.class).getValue(context);
      }catch(Exception x){
         throw new ServiceDiscoveryException(m, Messages.MESSAGES.failedToEvaluateELExpression(expression), x);
      }
   }

   public static Boolean evaluateELBoolean(Method m, ELContext context, Object base, String expression) {
      try{
         return (Boolean) EL.EXPRESSION_FACTORY.createValueExpression(context, expression,
               Boolean.class).getValue(context);
      }catch(Exception x){
         throw new ServiceDiscoveryException(m, Messages.MESSAGES.failedToEvaluateELExpression(expression), x);

      }
   }

}
