package org.jboss.resteasy.specimpl;

import org.jboss.resteasy.core.Headers;
import org.jboss.resteasy.plugins.delegates.LocaleDelegate;
import org.jboss.resteasy.resteasy_jaxrs.i18n.Messages;
import org.jboss.resteasy.spi.HeaderValueProcessor;
import org.jboss.resteasy.spi.HttpResponseCodes;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.util.CaseInsensitiveMap;
import org.jboss.resteasy.util.DateUtil;

import jakarta.ws.rs.core.EntityTag;
import jakarta.ws.rs.core.GenericEntity;
import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Link;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.core.Response;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/**
 * A response object not attached to a client or server invocation.
 *
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
@SuppressWarnings("rawtypes")
public abstract class AbstractBuiltResponse extends Response
{
   protected Object entity;
   protected int status = HttpResponseCodes.SC_OK;
   protected String reason = "Unknown Code";
   protected Headers<Object> metadata = new Headers<Object>();
   protected Annotation[] annotations;
   protected Class entityClass;
   protected Type genericType;
   protected HeaderValueProcessor processor;
   protected volatile boolean isClosed;
   protected InputStream is;
   protected byte[] bufferedEntity;
   protected volatile boolean streamRead;
   protected volatile boolean streamFullyRead;

   public AbstractBuiltResponse()
   {
   }

   public AbstractBuiltResponse(final int status, final String reason,
                        final Headers<Object> metadata, final Object entity,
                        final Annotation[] entityAnnotations)
   {
      setEntity(entity);
      this.status = status;
      this.metadata = metadata;
      this.annotations = entityAnnotations;
      if (reason != null) {
         this.reason = reason;
      }
   }

   protected abstract InputStream getInputStream();
   protected abstract void setInputStream(InputStream is);
   protected abstract InputStream getEntityStream();
   /**
    * Release underlying connection but do not close.
    *
    * @throws IOException if I/O error occurred
    */
   public abstract void releaseConnection() throws IOException;

   /**
    * Release underlying connection but do not close.
    *
    * @param consumeInputStream boolean to indicate either the underlying input stream must be fully read before releasing the connection or not.
    * <p>
    * For most HTTP connection implementations, consuming the underlying input stream before releasing the connection will help to ensure connection reusability with respect of Keep-Alive policy.
    * </p>
    * @throws IOException if I/O error occured
    */
   public abstract void releaseConnection(boolean consumeInputStream) throws IOException;


   public Class getEntityClass()
   {
      return entityClass;
   }

   public void setEntityClass(Class entityClass)
   {
      this.entityClass = entityClass;
   }

   protected HeaderValueProcessor getHeaderValueProcessor()
   {
      if (processor != null) return processor;
      return ResteasyProviderFactory.getInstance();
   }

   @Override
   public Object getEntity()
   {
      abortIfClosed();
      return entity;
   }

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

   public String getReasonPhrase()
   {
      return reason;
   }

   @Override
   public StatusType getStatusInfo()
   {
      StatusType statusType = Status.fromStatusCode(status);
      if (statusType == null)
      {
         statusType = new StatusType()
         {
            @Override
            public int getStatusCode()
            {
               return status;
            }

            @Override
            public Status.Family getFamily()
            {
               return Status.Family.familyOf(status);
            }

            @Override
            public String getReasonPhrase()
            {
               return reason;
            }
         };
      }
      return statusType;
   }

   @Override
   public MultivaluedMap<String, Object> getMetadata()
   {
      return metadata;
   }

   public void setEntity(Object entity)
   {
      if (entity == null)
      {
         this.entity = null;
         this.genericType = null;
         this.entityClass = null;
      }
      else if (entity instanceof GenericEntity)
      {

         GenericEntity ge = (GenericEntity) entity;
         this.entity = ge.getEntity();
         this.genericType = ge.getType();
         this.entityClass = ge.getRawType();
      }
      else
      {
         this.entity = entity;
         this.entityClass = entity.getClass();
         this.genericType = null;
      }
   }

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

   public void setReasonPhrase(String reason)
   {
      this.reason = reason;
   }

   public void setMetadata(MultivaluedMap<String, Object> metadata)
   {
      this.metadata = new Headers<Object>();
      this.metadata.putAll(metadata);
   }

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

   public void addMethodAnnotations(Annotation[] methodAnnotations)
   {
      List<Annotation> ann = new ArrayList<Annotation>();
      if (annotations != null)
      {
         for (Annotation annotation : annotations)
         {
            ann.add(annotation);
         }
      }
      for (Annotation annotation : methodAnnotations)
      {
         ann.add(annotation);
      }
      annotations = ann.toArray(new Annotation[ann.size()]);
   }


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

   public Type getGenericType()
   {
      return genericType;
   }

   public void setGenericType(Type genericType)
   {
      this.genericType = genericType;
   }

   @Override
   public <T> T readEntity(Class<T> type, Annotation[] annotations)
   {
      return readEntity(type, null, annotations);
   }

   @SuppressWarnings(value = "unchecked")
   @Override
   public <T> T readEntity(GenericType<T> entityType, Annotation[] annotations)
   {
      return readEntity((Class<T>) entityType.getRawType(), entityType.getType(), annotations);
   }

   @Override
   public <T> T readEntity(Class<T> type)
   {
      return readEntity(type, null, null);
   }


   @SuppressWarnings(value = "unchecked")
   @Override
   public <T> T readEntity(GenericType<T> entityType)
   {
      return readEntity((Class<T>) entityType.getRawType(), entityType.getType(), null);
   }

   public abstract <T> T readEntity(Class<T> type, Type genericType, Annotation[] anns);

   protected void resetEntity()
   {
      entity = null;
      bufferedEntity = null;
      streamFullyRead = false;
      streamRead = false;
   }

   public void setStreamRead(Boolean b)
   {
      streamRead = b;
   }

   public void setStreamFullyRead(Boolean b)
   {
      streamFullyRead = b;
   }

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

   public boolean isClosed()
   {
      return isClosed;
   }

   public void abortIfClosed()
   {
      if (bufferedEntity == null) {
         if (isClosed()) throw new IllegalStateException(Messages.MESSAGES.responseIsClosed());
      }
   }

   @Override
   public void close()
   {
      isClosed = true;
   }

   @Override
   public Locale getLanguage()
   {
      Object obj = metadata.getFirst(HttpHeaders.CONTENT_LANGUAGE);
      if (obj == null) return null;
      if (obj instanceof Locale) return (Locale) obj;
      return LocaleDelegate.INSTANCE.fromString(toHeaderString(obj));
   }

   @Override
   public int getLength()
   {
      Object obj = metadata.getFirst(HttpHeaders.CONTENT_LENGTH);
      if (obj == null) return -1;
      if (obj instanceof Integer) return (Integer) obj;
      return Integer.valueOf(toHeaderString(obj));
   }

   @Override
   public MediaType getMediaType()
   {
      Object obj = metadata.getFirst(HttpHeaders.CONTENT_TYPE);
      if (obj instanceof MediaType) return (MediaType) obj;
      if (obj == null) return null;
      return MediaType.valueOf(toHeaderString(obj));
   }

   @Override
   public Map<String, NewCookie> getCookies()
   {
      Map<String, NewCookie> cookies = new HashMap<String, NewCookie>();
      List list = metadata.get(HttpHeaders.SET_COOKIE);
      if (list == null) return cookies;
      for (Object obj : list)
      {
         if (obj instanceof NewCookie)
         {
            NewCookie cookie = (NewCookie)obj;
            cookies.put(cookie.getName(), cookie);
         }
         else
         {
            NewCookie cookie = createHeader(NewCookie.class, obj);
            cookies.put(cookie.getName(), cookie);
         }
      }
      return cookies;
   }

   @Override
   public EntityTag getEntityTag()
   {
      Object d = metadata.getFirst(HttpHeaders.ETAG);
      if (d == null) return null;
      if (d instanceof EntityTag) return (EntityTag) d;
      return createHeader(EntityTag.class, d);
   }

   @Override
   public Date getDate()
   {
      Object d = metadata.getFirst(HttpHeaders.DATE);
      if (d == null) return null;
      if (d instanceof Date) return (Date) d;
      return DateUtil.parseDate(d.toString());
   }

   @Override
   public Date getLastModified()
   {
      Object d = metadata.getFirst(HttpHeaders.LAST_MODIFIED);
      if (d == null) return null;
      if (d instanceof Date) return (Date) d;
      return DateUtil.parseDate(d.toString());
   }

   @Override
   public Set<String> getAllowedMethods()
   {
      Set<String> allowedMethods = new HashSet<String>();
      List<Object> allowed = metadata.get("Allow");
      if (allowed == null) return allowedMethods;
      for (Object header : allowed)
      {
         if (header != null && header instanceof String)
         {
            String[] list = ((String)header).split(",");
            for (String str : list)
            {
               if (!"".equals(str.trim()))
               {
                  allowedMethods.add(str.trim().toUpperCase());
               }
            }
         }
         else
         {
            allowedMethods.add(toHeaderString(header).toUpperCase());
         }
      }

      return allowedMethods;
   }

   protected String toHeaderString(Object header)
   {
      if (header instanceof String) return (String)header;
      return getHeaderValueProcessor().toHeaderString(header);
   }

   private <T> T createHeader(final Class<T> type, final Object header) {
      final String value;
      if (header instanceof String) {
         value = (String) header;
      } else {
         value = getHeaderValueProcessor().toHeaderString(header);
      }
      return ResteasyProviderFactory.getInstance()
              .createHeaderDelegate(type)
              .fromString(value);
   }

   @Override
   public MultivaluedMap<String, String> getStringHeaders()
   {
      CaseInsensitiveMap<String> map = new CaseInsensitiveMap<String>();
      for (Map.Entry<String, List<Object>> entry : metadata.entrySet())
      {
         for (Object obj : entry.getValue())
         {
            map.add(entry.getKey(), toHeaderString(obj));
         }
      }
      return map;
   }

   @Override
   public String getHeaderString(String name)
   {
      List vals = metadata.get(name);
      if (vals == null) return null;
      StringBuilder builder = new StringBuilder();
      boolean first = true;
      for (Object val : vals)
      {
         if (first) first = false;
         else builder.append(",");
         if (val == null) val = "";
         val = toHeaderString(val);
         if (val == null) val = "";
         builder.append(val);
      }
      return builder.toString();
   }

   @Override
   public URI getLocation()
   {
      Object uri = metadata.getFirst(HttpHeaders.LOCATION);
      if (uri == null) return null;
      if (uri instanceof URI) return (URI)uri;
      String str = null;
      if (uri instanceof String) str = (String)uri;
      else str = toHeaderString(uri);
      return URI.create(str);
   }

   @Override
   public Set<Link> getLinks()
   {
      LinkHeaders linkHeaders = getLinkHeaders();
      Set<Link> links = new HashSet<Link>();
      links.addAll(linkHeaders.getLinks());
      return links;
   }

   private LinkHeaders getLinkHeaders()
   {
      LinkHeaders linkHeaders = new LinkHeaders();
      linkHeaders.addLinkObjects(metadata, getHeaderValueProcessor());
      return linkHeaders;
   }

   @Override
   public boolean hasLink(String relation)
   {
      return getLinkHeaders().getLinkByRelationship(relation) != null;
   }

   @Override
   public Link getLink(String relation)
   {
      return getLinkHeaders().getLinkByRelationship(relation);
   }

   @Override
   public Link.Builder getLinkBuilder(String relation)
   {
      Link link = getLinkHeaders().getLinkByRelationship(relation);
      if (link == null) return null;
      return Link.fromLink(link);
   }


   private final class LinkHeaders
   {
      private Map<String, Link> linksByRelationship = new HashMap<String, Link>();
      private List<Link> links = new ArrayList<Link>();

      public LinkHeaders addLinkObjects(MultivaluedMap<String, Object> headers, HeaderValueProcessor factory)
      {
         List<Object> values = headers.get("Link");
         if (values == null) return this;


         for (Object val : values)
         {
            if (val instanceof Link) addLink((Link)val);
            else if (val instanceof String) {
               for (String link : ((String) val).split(",")) {
                  addLink(Link.valueOf(link));
               }
            } else {
               String str = factory.toHeaderString(val);
               addLink(Link.valueOf(str));
            }
         }
         return this;
      }

      public LinkHeaders addLink(final Link link)
      {
         links.add(link);
         for (String rel : link.getRels())
         {
            linksByRelationship.put(rel, link);
         }
         return this;
      }

      public Link getLinkByRelationship(String rel)
      {
         return linksByRelationship.get(rel);
      }

      /**
       * All the links defined.
       *
       * @return links
       */
      public List<Link> getLinks()
      {
         return links;
      }

   }

   protected static class InputStreamWrapper <T extends BuiltResponse>
           extends FilterInputStream
   {
      private T response;
      public InputStreamWrapper(final InputStream in, final T response) {
         super(in);
         this.response = response;
      }

      public int read() throws IOException
      {
         return checkEOF(super.read());
      }

      public int read(byte[] b) throws IOException
      {
         return checkEOF(super.read(b));
      }

      public int read(byte[] b, int off, int len) throws IOException
      {
         return checkEOF(super.read(b, off, len));
      }

      private int checkEOF(int v)
      {
         this.response.setStreamRead(true);
         if (v < 0)
         {
            this.response.setStreamFullyRead(true);
         }
         return v;
      }

      @Override
      public void close() throws IOException {
         super.close();
         this.response.close();
      }
   }

}
