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

import org.jboss.resteasy.client.jaxrs.i18n.Messages;
import org.jboss.resteasy.core.Headers;
import org.jboss.resteasy.core.ProvidersContextRetainer;
import org.jboss.resteasy.core.ResteasyContext;
import org.jboss.resteasy.core.interception.jaxrs.AbstractReaderInterceptorContext;
import org.jboss.resteasy.core.interception.jaxrs.ClientReaderInterceptorContext;
import org.jboss.resteasy.plugins.providers.sse.EventInput;
import org.jboss.resteasy.specimpl.AbstractBuiltResponse;
import org.jboss.resteasy.specimpl.BuiltResponse;
import org.jboss.resteasy.spi.HeaderValueProcessor;
import org.jboss.resteasy.spi.MarshalledEntity;
import org.jboss.resteasy.spi.util.Types;
import org.jboss.resteasy.tracing.RESTEasyTracingLogger;
import org.jboss.resteasy.util.HttpHeaderNames;
import org.jboss.resteasy.util.InputStreamToByteArray;
import org.jboss.resteasy.util.ReadFromStream;
import org.reactivestreams.Publisher;

import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.Providers;
import jakarta.ws.rs.ext.ReaderInterceptor;

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.Map;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public abstract class ClientResponse extends BuiltResponse
{
   // One thing to note, I don't cache header objects because I was too lazy to proxy the headers multivalued map
   protected Map<String, Object> properties;
   protected ClientConfiguration configuration;
   protected RESTEasyTracingLogger tracingLogger;

   @Deprecated
   protected ClientResponse(final ClientConfiguration configuration)
   {
      setClientConfiguration(configuration);
      tracingLogger = RESTEasyTracingLogger.empty();
   }

   protected ClientResponse(final ClientConfiguration configuration, final RESTEasyTracingLogger tracingLogger)
   {
      setClientConfiguration(configuration);
      this.tracingLogger = tracingLogger;
   }

   @SuppressWarnings({"rawtypes", "unchecked"})
   public void setHeaders(MultivaluedMap<String, String> headers)
   {
      this.metadata = new Headers<Object>();
      this.metadata.putAll((Map) headers);
   }

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

   public Map<String, Object> getProperties()
   {
      return properties;
   }

   public void setClientConfiguration(ClientConfiguration configuration)
   {
      this.configuration = configuration;
      this.processor = configuration;
   }

   @Override
   public synchronized Object getEntity()
   {
      abortIfClosed();
      Object entity = super.getEntity();
      if (entity != null)
      {
         return checkEntityReadAsInputStreamFullyConsumed(entity);
      }
      return checkEntityReadAsInputStreamFullyConsumed(getEntityStream());
   }

   //Check if the entity was previously fully consumed
   private <T> T checkEntityReadAsInputStreamFullyConsumed(T entity)
   {
      if (bufferedEntity == null && entity instanceof InputStream && streamFullyRead)
      {
         throw new IllegalStateException();
      }
      return entity;
   }

   @Override
   public Class<?> getEntityClass()
   {
      Class<?> classs = super.getEntityClass();
      if (classs != null)
      {
         return classs;
      }
      Object entity = null;
      try
      {
         entity = getEntity();
      }
      catch (Exception e)
      {
      }
      return entity != null ? entity.getClass() : null;
   }

   @Override
   public boolean hasEntity()
   {
      abortIfClosed();
      return getInputStream() != null && (entity != null || getMediaType() != null);
   }

   /**
    * In case of an InputStream or Reader and a invocation that returns no Response object, we need to make
    * sure the GC does not close the returned InputStream or Reader
    */
   public void noReleaseConnection()
   {

      isClosed = true;
   }

   @Override
   public void close()
   {
      if (isClosed()) return;
      try {
         isClosed = true;
         releaseConnection();
      }
      catch (Exception e) {
         throw new ProcessingException(e);
      }
   }

   @Override
   protected HeaderValueProcessor getHeaderValueProcessor()
   {
      return configuration;
   }

   protected InputStream getEntityStream()
   {
      if (bufferedEntity != null) return new ByteArrayInputStream(bufferedEntity);

      if (isClosed()) throw new ProcessingException(Messages.MESSAGES.streamIsClosed());
      InputStream is = getInputStream();
      return is != null ? new AbstractBuiltResponse.InputStreamWrapper<ClientResponse>(is, this) : null;
   }

   // Method is defined here because the "protected" abstract declaration
   // in AbstractBuiltResponse is not accessible to classes in this module.
   // Making the method "public" causes different errors.
   protected abstract void setInputStream(InputStream is);

   // this is synchronized in conjunction with finalize to protect against premature finalize called by the GC
   @Override
   protected synchronized <T> Object readFrom(Class<T> type, Type genericType,
                                    MediaType media, Annotation[] annotations)
   {
      Type useGeneric = genericType == null ? type : genericType;
      Class<?> useType = type;
      media = media == null ? MediaType.WILDCARD_TYPE : media;
      annotations = annotations == null ? this.annotations : annotations;
      boolean isMarshalledEntity = false;
      if (type.equals(MarshalledEntity.class))
      {
         isMarshalledEntity = true;
         ParameterizedType param = (ParameterizedType) useGeneric;
         useGeneric = param.getActualTypeArguments()[0];
         useType = Types.getRawType(useGeneric);
      }


      Providers current = ResteasyContext.getContextData(Providers.class);
      ResteasyContext.pushContext(Providers.class, configuration);
      Object obj = null;
      try
      {
         InputStream is = getEntityStream();
         if (is == null)
         {
            throw new IllegalStateException(Messages.MESSAGES.inputStreamWasEmpty());
         }
         if (isMarshalledEntity)
         {
            is = new InputStreamToByteArray(is);

         }

         ReaderInterceptor[] readerInterceptors = configuration.getReaderInterceptors(null, null);

         final Object finalObj;

         final long timestamp = tracingLogger.timestamp("RI_SUMMARY");
         AbstractReaderInterceptorContext context =
               new ClientReaderInterceptorContext(readerInterceptors, configuration.getProviderFactory(), useType,
                     useGeneric, annotations, media, getStringHeaders(), is, properties, tracingLogger);
         try {
            finalObj = context.proceed();
            obj = finalObj;
         } finally {
            tracingLogger.logDuration("RI_SUMMARY", timestamp, context.getProcessedInterceptorCount());
         }

         if (isMarshalledEntity)
         {
            InputStreamToByteArray isba = (InputStreamToByteArray) is;
            final byte[] bytes = isba.toByteArray();
            return new MarshalledEntity<Object>()
            {
               @Override
               public byte[] getMarshalledBytes()
               {
                  return bytes;
               }

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

      }
      catch (ProcessingException pe)
      {
         throw pe;
      }
      catch (Exception ex)
      {
         throw new ProcessingException(ex);
      }
      finally
      {
         if (!Publisher.class.isAssignableFrom(type) && !EventInput.class.isAssignableFrom(type))
         {
            ResteasyContext.popContextData(Providers.class);
            if (current != null)
               ResteasyContext.pushContext(Providers.class, current);
            if (obj instanceof ProvidersContextRetainer)
               ((ProvidersContextRetainer) obj).setProviders(configuration);
         }
      }
   }

   @Override
   public boolean bufferEntity()
   {
      abortIfClosed();
      if (bufferedEntity != null) return true;
      if (streamRead) return false;
      if (metadata.getFirst(HttpHeaderNames.CONTENT_TYPE) == null) return false;
      InputStream is = getInputStream();
      if (is == null) return false;
      try
      {
         bufferedEntity = ReadFromStream.readFromStream(1024, is);
      }
      catch (IOException e)
      {
         throw new ProcessingException(e);
      }
      finally
      {
         try {
            releaseConnection();
         }
         catch (IOException e) {
            throw new ProcessingException(e);
         }
      }
      return true;
   }

   @Override
   public void abortIfClosed()
   {
      if (bufferedEntity == null) super.abortIfClosed();
   }

}
