package org.jboss.resteasy.client.jaxrs.internal;

import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.core.interception.AbstractWriterInterceptorContext;
import org.jboss.resteasy.core.interception.ClientWriterInterceptorContext;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.util.DelegatingOutputStream;
import org.jboss.resteasy.util.Types;

import javax.ws.rs.BadRequestException;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.NotAcceptableException;
import javax.ws.rs.NotAllowedException;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.NotSupportedException;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.RedirectionException;
import javax.ws.rs.ServerErrorException;
import javax.ws.rs.ServiceUnavailableException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.ClientResponseFilter;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.InvocationCallback;
import javax.ws.rs.client.ResponseProcessingException;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Variant;
import javax.ws.rs.ext.Providers;
import javax.ws.rs.ext.WriterInterceptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class ClientInvocation implements Invocation
{
   protected ResteasyClient client;
   protected ClientRequestHeaders headers;
   protected String method;
   protected Object entity;
   protected Type entityGenericType;
   protected Class<?> entityClass;
   protected Annotation[] entityAnnotations;
   protected ClientConfiguration configuration;
   protected URI uri;
   protected boolean chunked;

   // todo need a better solution for this.  Apache Http Client 4 does not let you obtain the OutputStream before executing
   // this request. is problematic for obtaining and setting
   // the output stream.  It also does not let you modify the request headers before the output stream is available
   // Since MessageBodyWriter allows you to modify headers, you're s
   protected DelegatingOutputStream delegatingOutputStream = new DelegatingOutputStream();
   protected OutputStream entityStream = delegatingOutputStream;

   public ClientInvocation(ResteasyClient client, URI uri, ClientRequestHeaders headers, ClientConfiguration parent)
   {
      this.uri = uri;
      this.client = client;
      this.configuration = new ClientConfiguration(parent);
      this.headers = headers;
   }

   /**
    * Extracts result from response throwing an appropriate exception if not a successful response.
    *
    * @param responseType
    * @param response
    * @param annotations
    * @param <T>
    * @return
    */
   public static <T> T extractResult(GenericType<T> responseType, Response response, Annotation[] annotations)
   {
      int status = response.getStatus();
      if (status >= 200 && status < 300)
      {
         try
         {
            if (response.getMediaType() == null)
            {
               return null;
            }
            else
            {
               T rtn = response.readEntity(responseType, annotations);
               if (InputStream.class.isInstance(rtn)
                    || Reader.class.isInstance(rtn))
               {
                  if (response instanceof ClientResponse)
                  {
                     ClientResponse clientResponse = (ClientResponse)response;
                     clientResponse.noReleaseConnection();
                  }
               }
               return rtn;

            }
         }
         catch (WebApplicationException wae)
         {
            try
            {
               response.close();
            }
            catch (Exception e)
            {

            }
            throw wae;
         }
         catch (Throwable throwable)
         {
            try
            {
               response.close();
            }
            catch (Exception e)
            {

            }
            throw new ResponseProcessingException(response, throwable);
         }
         finally
         {
            if (response.getMediaType() == null) response.close();
         }
      }
      try
      {
         // Buffer the entity for any exception thrown as the response may have any entity the user wants
         // We don't want to leave the connection open though.
         String s = String.class.cast(response.getHeaders().getFirst("resteasy.buffer.exception.entity"));
         if (s == null || Boolean.parseBoolean(s))
         {
            response.bufferEntity();
         }
         else
         {
            // close connection
            if (response instanceof ClientResponse)
            {
               try
               {
                  ClientResponse.class.cast(response).releaseConnection();
               }
               catch (IOException e)
               {
                  // Ignore
               }
            }
         }
         if (status >= 300 && status < 400) throw new RedirectionException(response);

         return handleErrorStatus(response);
      }
      finally
      {
         // close if no content
         if (response.getMediaType() == null) response.close();
      }

   }

   /**
    * Throw an exception.  Expecting a status of 400 or greater.
    *
    * @param response
    * @param <T>
    * @return
    */
   public static <T> T handleErrorStatus(Response response)
   {
      final int status = response.getStatus();
      switch (status)
      {
         case 400:
            throw new BadRequestException(response);
         case 401:
            throw new NotAuthorizedException(response);
         case 403:
            throw new ForbiddenException(response);
         case 404:
            throw new NotFoundException(response);
         case 405:
            throw new NotAllowedException(response);
         case 406:
            throw new NotAcceptableException(response);
         case 415:
            throw new NotSupportedException(response);
         case 500:
            throw new InternalServerErrorException(response);
         case 503:
            throw new ServiceUnavailableException(response);
         default:
            break;
      }

      if (status >= 400 && status < 500) throw new ClientErrorException(response);
      if (status >= 500) throw new ServerErrorException(response);


      throw new WebApplicationException(response);
   }

   public ClientConfiguration getClientConfiguration()
   {
      return configuration;
   }

   public ResteasyClient getClient()
   {
      return client;
   }

   public DelegatingOutputStream getDelegatingOutputStream()
   {
      return delegatingOutputStream;
   }

   public void setDelegatingOutputStream(DelegatingOutputStream delegatingOutputStream)
   {
      this.delegatingOutputStream = delegatingOutputStream;
   }

   public OutputStream getEntityStream()
   {
      return entityStream;
   }

   public void setEntityStream(OutputStream entityStream)
   {
      this.entityStream = entityStream;
   }

   public URI getUri()
   {
      return uri;
   }

   public void setUri(URI uri)
   {
      this.uri = uri;
   }

   public Annotation[] getEntityAnnotations()
   {
      return entityAnnotations;
   }

   public void setEntityAnnotations(Annotation[] entityAnnotations)
   {
      this.entityAnnotations = entityAnnotations;
   }

   public String getMethod()
   {
      return method;
   }

   public void setMethod(String method)
   {
      this.method = method;
   }

   public void setHeaders(ClientRequestHeaders headers)
   {
      this.headers = headers;
   }

   public Map<String, Object> getMutableProperties()
   {
      return configuration.getMutableProperties();
   }

   public Object getEntity()
   {
      return entity;
   }

   public Type getEntityGenericType()
   {
      return entityGenericType;
   }

   public Class<?> getEntityClass()
   {
      return entityClass;
   }

   public ClientRequestHeaders getHeaders()
   {
      return headers;
   }

   public void setEntity(Entity<?> entity)
   {
      if (entity == null)
      {
         this.entity = null;
         this.entityAnnotations = null;
         this.entityClass = null;
         this.entityGenericType = null;
      }
      else
      {
         Object ent = entity.getEntity();
         setEntityObject(ent);
         this.entityAnnotations = entity.getAnnotations();
         Variant v = entity.getVariant();
         headers.setMediaType(v.getMediaType());
         headers.setLanguage(v.getLanguage());
         headers.header("Content-Encoding", null);
         headers.header("Content-Encoding", v.getEncoding());
      }

   }

   public void setEntityObject(Object ent)
   {
      if (ent instanceof GenericEntity)
      {
         GenericEntity<?> genericEntity = (GenericEntity<?>) ent;
         entityClass = genericEntity.getRawType();
         entityGenericType = genericEntity.getType();
         this.entity = genericEntity.getEntity();
      }
      else
      {
         if (ent == null)
         {
            this.entity = null;
            this.entityClass = null;
            this.entityGenericType = null;  
         }
         else
         {
            this.entity = ent;
            this.entityClass = ent.getClass();
            this.entityGenericType = ent.getClass();
         }
      }
   }

   public void writeRequestBody(OutputStream outputStream) throws IOException
   {
      if (entity == null)
      {
         return;
      }

      WriterInterceptor[] interceptors = getWriterInterceptors();
      AbstractWriterInterceptorContext ctx = new ClientWriterInterceptorContext(interceptors, configuration.getProviderFactory(), entity, entityClass, entityGenericType, entityAnnotations, headers.getMediaType(), headers.getHeaders(), outputStream, getMutableProperties());
      ctx.proceed();
   }


   public WriterInterceptor[] getWriterInterceptors()
   {
      return configuration.getWriterInterceptors(null, null);
   }

   public ClientRequestFilter[] getRequestFilters()
   {
      return configuration.getRequestFilters(null, null);
   }

   public ClientResponseFilter[] getResponseFilters()
   {
      return configuration.getResponseFilters(null, null);
   }

   // Invocation methods


   public Configuration getConfiguration()
   {
      return configuration;
   }

   public boolean isChunked()
   {
      return chunked;
   }

   public void setChunked(boolean chunked)
   {
      this.chunked = chunked;
   }

   @Override
   public Response invoke()
   {
      Providers current = ResteasyProviderFactory.getContextData(Providers.class);
      ResteasyProviderFactory.pushContext(Providers.class, configuration);
      try
      {
         ClientRequestContextImpl requestContext = new ClientRequestContextImpl(this);
         ClientRequestFilter[] requestFilters = getRequestFilters();
         ClientResponse aborted = null;
         if (requestFilters != null && requestFilters.length > 0)
         {
            for (ClientRequestFilter filter : requestFilters)
            {
               try
               {
                  filter.filter(requestContext);
                  if (requestContext.getAbortedWithResponse() != null)
                  {
                     aborted = new AbortedResponse(configuration, requestContext.getAbortedWithResponse());
                     break;
                  }
               }
               catch (ProcessingException e)
               {
                  throw e;
               }
               catch (Throwable e)
               {
                  throw new ProcessingException(e);
               }
            }
         }
         // spec requires that aborted response go through filter/interceptor chains.
         ClientResponse response = aborted;
         if (response == null) response = client.httpEngine().invoke(this);
         response.setProperties(configuration.getMutableProperties());

         ClientResponseFilter[] responseFilters = getResponseFilters();
         if (responseFilters != null && responseFilters.length > 0)
         {
            ClientResponseContextImpl responseContext = new ClientResponseContextImpl(response);
            for (ClientResponseFilter filter : responseFilters)
            {
               try
               {
                  filter.filter(requestContext, responseContext);
               }
               catch (ResponseProcessingException e)
               {
                  throw e;
               }
               catch (Throwable e)
               {
                  throw new ResponseProcessingException(response, e);
               }
            }
         }
         return response;
      }
      finally
      {
         ResteasyProviderFactory.popContextData(Providers.class);
         if (current != null) ResteasyProviderFactory.pushContext(Providers.class, current);
      }
   }

   @SuppressWarnings("unchecked")
   @Override
   public <T> T invoke(Class<T> responseType)
   {
      Response response = invoke();
      if (Response.class.equals(responseType)) return (T)response;
      return extractResult(new GenericType<T>(responseType), response, null);
   }

   @SuppressWarnings("unchecked")
   @Override
   public <T> T invoke(GenericType<T> responseType)
   {
      Response response = invoke();
      if (responseType.getRawType().equals(Response.class)) return (T)response;
      return ClientInvocation.extractResult(responseType, response, null);
   }

   @Override
   public Future<Response> submit()
   {
      return client.asyncInvocationExecutor().submit(new Callable<Response>()
      {
         @Override
         public Response call() throws Exception
         {
            return invoke();
         }
      });
   }


   @Override
   public <T> Future<T> submit(final Class<T> responseType)
   {
      return client.asyncInvocationExecutor().submit(new Callable<T>()
      {
         @Override
         public T call() throws Exception
         {
            return invoke(responseType);
         }
      });
   }

  @Override
   public <T> Future<T> submit(final GenericType<T> responseType)
   {
      return client.asyncInvocationExecutor().submit(new Callable<T>()
      {
         @Override
         public T call() throws Exception
         {
            return invoke(responseType);
         }
      });
   }

   @SuppressWarnings({ "rawtypes", "unchecked" })
   @Override
   public <T> Future<T> submit(final InvocationCallback<T> callback)
   {
      GenericType<T> genericType = (GenericType<T>)new GenericType<Object>() {};
      Type[] typeInfo = Types.getActualTypeArgumentsOfAnInterface(callback.getClass(), InvocationCallback.class);
      if (typeInfo != null)
      {
         genericType = new GenericType(typeInfo[0]);
      }

      final GenericType<T> responseType = genericType;
      return client.asyncInvocationExecutor().submit(new Callable<T>()
      {
         @Override
         public T call() throws Exception
         {
            T result = null;
            try {
               result = invoke(responseType);
            }
            catch (Exception e) {
               callback.failed(e);
               throw e;
            }
            try {
               callback.completed(result);
               return result;
            }
            finally {
               if (result != null && result instanceof Response)
               {
                  ((Response)result).close();
               }
            }
         }
      });

   }

   @Override
   public Invocation property(String name, Object value)
   {
      configuration.property(name, value);
      return this;
   }
}
