package org.jboss.resteasy.client.core.extractors;

import org.jboss.resteasy.annotations.ClientResponseType;
import org.jboss.resteasy.annotations.ResponseObject;
import org.jboss.resteasy.client.ClientResponse;
import org.jboss.resteasy.client.ClientResponseFailure;
import org.jboss.resteasy.client.EntityTypeFactory;
import org.jboss.resteasy.client.core.BaseClientResponse;
import org.jboss.resteasy.util.Types;

import javax.ws.rs.core.Response;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

/**
 * @author Solomon.Duskis
 */
@SuppressWarnings("unchecked")
public class DefaultEntityExtractorFactory implements EntityExtractorFactory
{

   public static final EntityExtractor clientResponseExtractor = new EntityExtractor<ClientResponse>()
   {
      public ClientResponse extractEntity(ClientRequestContext context, Object... args)
      {
         return context.getClientResponse();
      }
   };

   public static EntityExtractor<Response.Status> createStatusExtractor(final boolean release)
   {
      return new EntityExtractor<Response.Status>()
      {
         public Response.Status extractEntity(ClientRequestContext context, Object... args)
         {
            if (release)
               context.getClientResponse().releaseConnection();
            return context.getClientResponse().getResponseStatus();
         }
      };
   }

   public static final EntityExtractor createVoidExtractor(final boolean release)
   {
      return new EntityExtractor()
      {
         public Object extractEntity(ClientRequestContext context, Object... args)
         {
            try
            {
               context.getClientResponse().checkFailureStatus();
            }
            catch (ClientResponseFailure ce)
            {
               // If ClientResponseFailure do a copy of the response and then release the connection,
               // we need to use the copy here and not the original response
               context.getErrorHandler().clientErrorHandling((BaseClientResponse) ce.getResponse(), ce);
            }
            catch (RuntimeException e)
            {
               context.getErrorHandler().clientErrorHandling(context.getClientResponse(), e);
            }
            if (release)
               context.getClientResponse().releaseConnection();
            return null;
         }
      };
   }

   public EntityExtractor createExtractor(final Method method)
   {
      final Class returnType = method.getReturnType();
      if (isVoidReturnType(returnType))
         return createVoidExtractor(true);
      if (returnType.equals(Response.Status.class))
         return createStatusExtractor(true);

      if (Response.class.isAssignableFrom(returnType))
         return createResponseTypeEntityExtractor(method);

      if (returnType.isInterface() && returnType.isAnnotationPresent(ResponseObject.class))
         return new ResponseObjectProxy(method, new ResponseObjectEntityExtractorFactory());

      // We are not a ClientResponse type so we need to unmarshall and narrow it
      // to right type. If we are unable to unmarshall, or encounter any kind of
      // Exception, give the ClientErrorHandlers a chance to handle the
      // ClientResponse manually.

      return new BodyEntityExtractor(method);
   }

   protected EntityExtractor createResponseTypeEntityExtractor(final Method method)
   {
      final ClientResponseType responseHint = method.getAnnotation(ClientResponseType.class);
      if (responseHint != null)
      {
         final Class responseHintReturnType = responseHint.entityType();
         if (isVoidReturnType(responseHintReturnType))
         {
            final Class<? extends EntityTypeFactory> entityTypeFactory = responseHint.entityTypeFactory();
            return new EntityExtractor()
            {
               public Object extractEntity(ClientRequestContext context, Object... args)
               {
                  EntityTypeFactory factory = null;
                  try
                  {
                     factory = entityTypeFactory.newInstance();
                  }
                  catch (InstantiationException e)
                  {
                     throw (context.getClientResponse())
                             .createResponseFailure("Could not create a default entity type factory of type "
                                     + entityTypeFactory.getClass().getName());
                  }
                  catch (IllegalAccessException e)
                  {
                     throw ((BaseClientResponse<?>) context.getClientResponse())
                             .createResponseFailure("Could not create a default entity type factory of type "
                                     + entityTypeFactory.getClass().getName() + ". " + e.getMessage());
                  }
                  context.getClientResponse().setReturnType(
                          factory.getEntityType(((BaseClientResponse<?>) context.getClientResponse()).getStatus(),
                                  ((BaseClientResponse<?>) context.getClientResponse()).getMetadata()));
                  return context.getClientResponse();
               }
            };
         }
         else
         {
            return new EntityExtractor()
            {
               public Object extractEntity(ClientRequestContext context, Object... args)
               {
                  context.getClientResponse().setReturnType(responseHintReturnType);
                  return context.getClientResponse();
               }
            };
         }
      }
      else
      {
         final Type methodGenericReturnType = method.getGenericReturnType();
         if (methodGenericReturnType instanceof ParameterizedType)
         {
            final ParameterizedType zType = (ParameterizedType) methodGenericReturnType;
            final Type genericReturnType = zType.getActualTypeArguments()[0];
            final Class<?> responseReturnType = Types.getRawType(genericReturnType);
            return new EntityExtractor()
            {
               public Object extractEntity(ClientRequestContext context, Object... args)
               {
                  context.getClientResponse().setReturnType(responseReturnType);
                  context.getClientResponse().setGenericReturnType(genericReturnType);
                  return context.getClientResponse();
               }
            };
         }
         else
            return clientResponseExtractor;
      }
   }

   public static final boolean isVoidReturnType(Class<?> returnType)
   {
      return returnType == null || void.class.equals(returnType) || Void.class.equals(returnType);
   }

}
