package org.jboss.resteasy.client.core;

import org.jboss.resteasy.client.ClientExecutor;
import org.jboss.resteasy.client.ClientResponse;
import org.jboss.resteasy.client.ClientResponseFailure;
import org.jboss.resteasy.plugins.delegates.LinkHeaderDelegate;
import org.jboss.resteasy.spi.Link;
import org.jboss.resteasy.spi.LinkHeader;
import org.jboss.resteasy.spi.MarshalledEntity;
import org.jboss.resteasy.spi.ReaderException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.spi.interception.MessageBodyReaderInterceptor;
import org.jboss.resteasy.util.CaseInsensitiveMap;
import org.jboss.resteasy.util.GenericType;
import org.jboss.resteasy.util.HttpHeaderNames;
import org.jboss.resteasy.util.HttpResponseCodes;
import org.jboss.resteasy.util.InputStreamToByteArray;
import org.jboss.resteasy.util.ReadFromStream;
import org.jboss.resteasy.util.Types;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;

import static java.lang.String.*;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
@SuppressWarnings("unchecked")
public class BaseClientResponse<T> extends ClientResponse<T>
{
   public static interface BaseClientResponseStreamFactory
   {
      InputStream getInputStream() throws IOException;

      void performReleaseConnection();
   }

   protected ResteasyProviderFactory providerFactory;
   protected String attributeExceptionsTo;
   protected MultivaluedMap<String, String> headers;
   protected String alternateMediaType;
   protected Class<?> returnType;
   protected Type genericReturnType;
   protected Annotation[] annotations = {};
   protected int status;
   protected boolean wasReleased = false;
   protected Object unmarshaledEntity;
   protected MessageBodyReaderInterceptor[] messageBodyReaderInterceptors;
   protected Exception exception;// These can only be set by an interceptor
   protected BaseClientResponseStreamFactory streamFactory;
   protected LinkHeader linkHeader;
   protected Link location;
   protected ClientExecutor executor;
   protected Map<String, Object> attributes;

   public BaseClientResponse(BaseClientResponseStreamFactory streamFactory, ClientExecutor executor)
   {
      this.streamFactory = streamFactory;
      this.executor = executor;
   }

   public BaseClientResponse(BaseClientResponseStreamFactory streamFactory)
   {
      this.streamFactory = streamFactory;
   }

   /**
    * Store entity within a byte array input stream because we want to release the connection
    * if a ClientResponseFailure is thrown.  Copy status and headers, but ignore
    * all type information stored in the ClientResponse.
    *
    * @param copy
    * @return
    */
   public static ClientResponse copyFromError(ClientResponse copy)
   {
      if (copy instanceof BaseClientResponse)
      {
         BaseClientResponse base = (BaseClientResponse) copy;
         InputStream is = null;
         if (copy.getHeaders().containsKey("Content-Type"))
         {
            try
            {
               is = base.streamFactory.getInputStream();
               byte[] bytes = ReadFromStream.readFromStream(1024, is);
               is = new ByteArrayInputStream(bytes);
            }
            catch (IOException e)
            {
               // ignored
            }
         }
         final InputStream theIs = is;
         BaseClientResponse tmp = new BaseClientResponse(new BaseClientResponse.BaseClientResponseStreamFactory()
         {
            InputStream stream;

            public InputStream getInputStream() throws IOException
            {
               return theIs;
            }

            public void performReleaseConnection()
            {
            }
         });
         tmp.executor = base.executor;
         tmp.status = base.status;
         tmp.providerFactory = base.providerFactory;
         tmp.headers = new CaseInsensitiveMap<String>();
         tmp.headers.putAll(base.headers);
         tmp.messageBodyReaderInterceptors = base.messageBodyReaderInterceptors;
         return tmp;
      }
      else
      {
         // not sure why this code path would ever be executed!
         InputStream is = null;
         if (copy.getHeaders().containsKey("Content-Type"))
         {
            GenericType<byte[]> gt = new GenericType<byte[]>()
            {
            };
            try
            {
               byte[] bytes = (byte[]) copy.getEntity(gt);
               is = new ByteArrayInputStream(bytes);
            }
            catch (Exception ignore)
            {
            }
         }
         final InputStream theIs = is;
         BaseClientResponse tmp = new BaseClientResponse(new BaseClientResponse.BaseClientResponseStreamFactory()
         {
            InputStream stream;

            public InputStream getInputStream() throws IOException
            {
               return theIs;
            }

            public void performReleaseConnection()
            {
            }
         });
         tmp.status = copy.getStatus();
         tmp.providerFactory = ResteasyProviderFactory.getInstance();
         tmp.headers = new CaseInsensitiveMap<String>();
         tmp.headers.putAll(copy.getHeaders());
         tmp.headers.remove("Content-Encoding"); // remove encoding because we will have already extracted byte array
         return tmp;
      }
   }

   @Override
   public Map<String, Object> getAttributes()
   {
      return attributes;
   }

   public void setAttributes(Map<String, Object> attributes)
   {
      this.attributes = attributes;
   }

   public void setMessageBodyReaderInterceptors(MessageBodyReaderInterceptor[] messageBodyReaderInterceptors)
   {
      this.messageBodyReaderInterceptors = messageBodyReaderInterceptors;
   }

   public void setStatus(int status)
   {
      this.status = status;
   }

   public void setHeaders(MultivaluedMap<String, String> headers)
   {
      this.headers = headers;
   }

   public void setProviderFactory(ResteasyProviderFactory providerFactory)
   {
      this.providerFactory = providerFactory;
   }

   public void setReturnType(Class<T> returnType)
   {
      this.returnType = returnType;
   }

   public Class<?> getReturnType()
   {
      return returnType;
   }

   public void setGenericReturnType(Type genericReturnType)
   {
      this.genericReturnType = genericReturnType;
   }

   public void setAnnotations(Annotation[] annotations)
   {
      this.annotations = annotations;
   }

   public String getAttributeExceptionsTo()
   {
      return attributeExceptionsTo;
   }

   public void setAttributeExceptionsTo(String attributeExceptionsTo)
   {
      this.attributeExceptionsTo = attributeExceptionsTo;
   }

   public Exception getException()
   {
      return exception;
   }

   public void setException(Exception exception)
   {
      this.exception = exception;
   }

   public Annotation[] getAnnotations()
   {
      return this.annotations;
   }

   public String getResponseHeader(String headerKey)
   {
      if (headers == null) return null;
      return headers.getFirst(headerKey);
   }

   public LinkHeader getLinkHeader()
   {
      if (linkHeader != null) return linkHeader;
      linkHeader = new LinkHeader();
      if (!headers.containsKey("Link"))
      {
         return linkHeader;
      }
      List<String> links = headers.get("Link");
      LinkHeaderDelegate delegate = new LinkHeaderDelegate();
      for (String link : links)
      {
         LinkHeader tmp = delegate.fromString(link);
         linkHeader.getLinks().addAll(tmp.getLinks());
         linkHeader.getLinksByRelationship().putAll(tmp.getLinksByRelationship());
         linkHeader.getLinksByTitle().putAll(tmp.getLinksByTitle());

      }
      for (Link link : linkHeader.getLinks())
      {
         link.setExecutor(executor);
      }
      return linkHeader;
   }

   @Override
   public Link getLocation()
   {
      if (location != null) return location;
      if (!headers.containsKey("Location")) return null;
      String header = headers.getFirst("Location");

      location = new Link();
      location.setHref(header);
      location.setExecutor(executor);

      return location;
   }

   @Override
   public Link getHeaderAsLink(String headerName)
   {
      String value = headers.getFirst(headerName);
      if (value == null) return null;
      String type = headers.getFirst(headerName + "-type");
      Link link = new Link();
      link.setHref(value);
      link.setType(type);
      link.setExecutor(executor);
      return link;
   }

   public void setAlternateMediaType(String alternateMediaType)
   {
      this.alternateMediaType = alternateMediaType;
   }

   public BaseClientResponseStreamFactory getStreamFactory()
   {
      return streamFactory;
   }

   public void setStreamFactory(BaseClientResponseStreamFactory streamFactory)
   {
      this.streamFactory = streamFactory;
   }

   @Override
   public void resetStream()
   {
      try
      {
         this.streamFactory.getInputStream().reset();
      }
      catch (IOException e)
      {
         throw new RuntimeException(e);
      }
   }

   @Override
   public T getEntity()
   {
      if (returnType == null)
      {
         throw new RuntimeException(
                 "No type information to extract entity with, use other getEntity() methods");
      }
      return (T) getEntity(returnType, genericReturnType, this.annotations);
   }

   @Override
   public <T2> T2 getEntity(Class<T2> type)
   {
      return getEntity(type, null);
   }

   @Override
   public <T2> T2 getEntity(Class<T2> type, Type genericType)
   {
      return getEntity(type, genericType, getAnnotations(type, genericType));
   }

   private <T2> Annotation[] getAnnotations(Class<T2> type, Type genericType)
   {
      return (this.returnType == type && this.genericReturnType == genericType) ? this.annotations
              : null;
   }

   @Override
   public <T2> T2 getEntity(Class<T2> type, Type genericType, Annotation[] anns)
   {
      if (exception != null)
      {
         throw new RuntimeException("Unable to unmarshall response for "
                 + attributeExceptionsTo, exception);
      }

      if (unmarshaledEntity != null && !type.isInstance(this.unmarshaledEntity))
         throw new RuntimeException("The entity was already read, and it was of type "
                 + unmarshaledEntity.getClass());

      if (unmarshaledEntity == null)
      {
         if (status == HttpResponseCodes.SC_NO_CONTENT)
            return null;

         unmarshaledEntity = readFrom(type, genericType, getMediaType(), anns);
         // only release connection if we actually unmarshalled something and if the object is *NOT* an InputStream
         // If it is an input stream, the user may be doing their own stream processing.
         if (unmarshaledEntity != null && !InputStream.class.isInstance(unmarshaledEntity)) releaseConnection();
      }
      return (T2) unmarshaledEntity;
   }

   protected MediaType getMediaType()
   {
      String mediaType = getResponseHeader(HttpHeaderNames.CONTENT_TYPE);
      if (mediaType == null)
      {
         mediaType = alternateMediaType;
      }

      return mediaType == null ? MediaType.WILDCARD_TYPE : MediaType.valueOf(mediaType);
   }

   protected <T2> Object readFrom(Class<T2> type, Type genericType,
                                  MediaType media, Annotation[] annotations)
   {
      Type useGeneric = genericType == null ? type : genericType;
      Class<?> useType = type;
      boolean isMarshalledEntity = false;
      if (type.equals(MarshalledEntity.class))
      {
         isMarshalledEntity = true;
         ParameterizedType param = (ParameterizedType) useGeneric;
         useGeneric = param.getActualTypeArguments()[0];
         useType = Types.getRawType(useGeneric);
      }


      MessageBodyReader reader1 = providerFactory.getMessageBodyReader(useType,
              useGeneric, this.annotations, media);
      if (reader1 == null)
      {
         throw createResponseFailure(format(
                 "Unable to find a MessageBodyReader of content-type %s and type %s",
                 media, genericType));
      }

      try
      {
         InputStream is = streamFactory.getInputStream();
         if (is == null)
         {
            throw new ClientResponseFailure("Input stream was empty, there is no entity", this);
         }
         if (isMarshalledEntity)
         {
            is = new InputStreamToByteArray(is);

         }

         final Object obj = new ClientMessageBodyReaderContext(messageBodyReaderInterceptors, reader1, useType,
                 useGeneric, this.annotations, media, getHeaders(), is, attributes)
                 .proceed();
         if (isMarshalledEntity)
         {
            InputStreamToByteArray isba = (InputStreamToByteArray) is;
            final byte[] bytes = isba.toByteArray();
            return new MarshalledEntity()
            {
               @Override
               public byte[] getMarshalledBytes()
               {
                  return bytes;
               }

               @Override
               public Object getEntity()
               {
                  return obj;
               }
            };
         }
         else
         {
            return (T2) obj;
         }

      }
      catch (Exception e)
      {
         if (e instanceof ReaderException)
         {
            throw (ReaderException) e;
         }
         else
         {
            throw new ReaderException(e);
         }
      }
   }

   @Override
   public <T2> T2 getEntity(GenericType<T2> genericType)
   {
      return getEntity(genericType.getType(), genericType.getGenericType());
   }

   @Override
   public <T2> T2 getEntity(GenericType<T2> genericType, Annotation[] ann)
   {
      return getEntity(genericType.getType(), genericType.getGenericType(), ann);
   }

   public MultivaluedMap<String, String> getHeaders()
   {
      return headers;
   }

   @Override
   public MultivaluedMap<String, Object> getMetadata()
   {
      // hack to cast from <String, String> to <String, Object>
      return (MultivaluedMap) headers;
   }

   @Override
   public int getStatus()
   {
      return status;
   }

   public void checkFailureStatus()
   {
      if (status > 399 && status < 599)
      {
         throw createResponseFailure(format("Error status %d %s returned", status, getResponseStatus()));
      }
   }

   public ClientResponseFailure createResponseFailure(String message)
   {
      return createResponseFailure(message, null);
   }

   public ClientResponseFailure createResponseFailure(String message, Exception e)
   {
      setException(e);
      this.returnType = byte[].class;
      this.genericReturnType = null;
      this.annotations = null;
      return new ClientResponseFailure(message, e, (ClientResponse<byte[]>) this);
   }

   @Override
   public Status getResponseStatus()
   {
      return Status.fromStatusCode(getStatus());
   }

   public boolean wasReleased()
   {
      return wasReleased;
   }

   public final void releaseConnection()
   {
      if (!wasReleased)
      {
         if (streamFactory != null) streamFactory.performReleaseConnection();
         wasReleased = true;
      }
   }

   @Override
   protected final void finalize() throws Throwable
   {
      releaseConnection();
   }

}
