package org.jboss.resteasy.plugins.cache.server;


import org.infinispan.Cache;
import org.jboss.resteasy.specimpl.MultivaluedTreeMap;

import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * An HTTP cache that behaves somewhat the same way as a proxy (like Squid)
 *
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class InfinispanCache implements ServerCache
{

   public static class CacheEntry implements Entry, Serializable
   {
      private static final long serialVersionUID = 2848638331930090578L;

      private byte[] cached;
      private int expires;
      private long timestamp = System.currentTimeMillis();
      private String etag;
      private transient MultivaluedMap<String, Object> headers;
      private transient MediaType mediaType;
      private transient MultivaluedMap<String, String> varyHeaders;

      private CacheEntry(final MultivaluedMap<String, Object> headers, final byte[] cached, final int expires, final String etag, final MediaType mediaType, final MultivaluedMap<String, String> varyHeaders)
      {
         this.cached = cached;
         this.expires = expires;
         this.headers = headers;
         this.etag = etag;
         this.mediaType = mediaType;
         this.varyHeaders = varyHeaders;
      }

      public int getExpirationInSeconds()
      {
         return expires - (int) ((System.currentTimeMillis() - timestamp) / 1000);
      }

      public boolean isExpired()
      {
         return System.currentTimeMillis() - timestamp >= expires * 1000L;
      }

      public String getEtag()
      {
         return etag;
      }

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

      public MultivaluedMap<String, String> getVaryHeaders()
      {
         return varyHeaders;
      }

      public byte[] getCached()
      {
         return cached;
      }

      public MediaType getMediaType()
      {
         return mediaType;
      }

      private void writeObject(ObjectOutputStream stream) throws IOException
      {
         stream.defaultWriteObject();
         stream.writeObject(stringifyHeaders(headers));
         stream.writeObject(stringifyHeaders(varyHeaders));
         stream.writeUTF(mediaType.toString());
      }

      @SuppressWarnings("unchecked")
      private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException
      {
         stream.defaultReadObject();
         headers = unstringifyHeaders(MultivaluedMap.class.cast(stream.readObject()));
         varyHeaders = unstringifyHeaders(MultivaluedMap.class.cast(stream.readObject()));
         mediaType = MediaType.valueOf(stream.readUTF());
      }
   }

   @SuppressWarnings("rawtypes")
   protected Cache cache;

   @SuppressWarnings("rawtypes")
   public InfinispanCache(final Cache cache)
   {
      this.cache = cache;
   }

   public Entry get(String uri, MediaType accept, MultivaluedMap<String, String> headers)
   {
      @SuppressWarnings("unchecked")
      Set<String> entries = (Set<String>)cache.get(uri);
      if (entries == null) return null;

      for (String entry : entries)
      {
         CacheEntry cacheEntry = (CacheEntry)cache.get(entry);
         if (cacheEntry == null) continue;
         if (accept.isCompatible(cacheEntry.getMediaType()) && !ServerCache.mayVary(cacheEntry, headers))
         {
            return cacheEntry;
         }
      }
      return null;
   }

   @SuppressWarnings("unchecked")
   public Entry add(String uri, MediaType mediaType, CacheControl cc, MultivaluedMap<String, Object> headers, byte[] entity, String etag, MultivaluedMap<String, String> varyHeaders)
   {
      // there's a race condition here with a concurrent get() method above.  Too bad JBoss Cache doesn't have a way to create
      // a node before hand then insert it
      CacheEntry cacheEntry = new CacheEntry(headers, entity, cc.getMaxAge(), etag, mediaType, varyHeaders);
      StringBuffer varyHeadersString = new StringBuffer();
      varyHeaders.forEach((name, values) -> values.forEach(value -> varyHeadersString.append(name).append(value)));
      String entryName = uri + "    " + mediaType.toString() + "    " + varyHeadersString.toString();
      Set<String> entries = (Set<String>)cache.get(uri);
      Set<String> newEntries = new HashSet<String>();
      newEntries.add(entryName);
      if (entries != null)
      {
         newEntries.addAll(entries);
      }
      cache.put(uri, newEntries);
      cache.put(entryName, cacheEntry, cc.getMaxAge(), TimeUnit.SECONDS);
      return cacheEntry;
   }

   public void remove(String uri)
   {
      @SuppressWarnings("unchecked")
      Set<String> entries = (Set<String>)cache.remove(uri);
      if (entries == null) return;
      for (String entry : entries)
      {
         cache.remove(entry);
      }
   }

   public void clear()
   {
      cache.clear();
   }

   protected static MultivaluedMap<String, ?> stringifyHeaders(MultivaluedMap<String, ?> headers)
   {
      MultivaluedMap<String, Object> holders = new MultivaluedTreeMap<String, Object>();
      for (Iterator<String> it1 = headers.keySet().iterator(); it1.hasNext(); )
      {
         String key = it1.next();
         List<Object> outList = new ArrayList<Object>();
         holders.put(key, outList);
         List<?> list = headers.get(key);
         for (Iterator<?> it2 = list.iterator(); it2.hasNext(); )
         {
            Object o = it2.next();
            if (o instanceof CacheControl)
            {
               outList.add(new HeaderHolder(HeaderHolder.Type.CACHE_CONTROL, CacheControl.class.cast(o).toString()));
            }
            else if (o instanceof NewCookie)
            {
               outList.add(new HeaderHolder(HeaderHolder.Type.NEW_COOKIE, NewCookie.class.cast(o).toString()));
            }
            else if (o instanceof Cookie)
            {
               outList.add(new HeaderHolder(HeaderHolder.Type.COOKIE, Cookie.class.cast(o).toString()));
            }
            else if (o instanceof EntityTag)
            {
               outList.add(new HeaderHolder(HeaderHolder.Type.ENTITY_TAG, EntityTag.class.cast(o).toString()));
            }
            else
            {
               outList.add(new HeaderHolder(HeaderHolder.Type.OTHER, o.toString()));
            }
         }
      }
      return holders;
   }


   protected static MultivaluedMap<String, Object> unstringifyHeaders(MultivaluedMap<String, Object> headers)
   {
      MultivaluedMap<String, Object> holders = new MultivaluedTreeMap<String, Object>();
      for (Iterator<String> it1 = headers.keySet().iterator(); it1.hasNext(); )
      {
         String key = it1.next();
         List<Object> outList = new ArrayList<Object>();
         holders.put(key, outList);
         List<Object> list = headers.get(key);
         for (Iterator<Object> it2 = list.iterator(); it2.hasNext(); )
         {
            HeaderHolder holder = HeaderHolder.class.cast(it2.next());
            if (HeaderHolder.Type.CACHE_CONTROL.equals(holder.getType()))
            {
               outList.add(CacheControl.valueOf(holder.getValue()));
            }
            else if (HeaderHolder.Type.COOKIE.equals(holder.getType()))
            {
               outList.add(Cookie.valueOf(holder.getValue()));
            }
            else if (HeaderHolder.Type.ENTITY_TAG.equals(holder.getType()))
            {
               outList.add(EntityTag.valueOf(holder.getValue()));
            }
            else if (HeaderHolder.Type.NEW_COOKIE.equals(holder.getType()))
            {
               outList.add(NewCookie.valueOf(holder.getValue()));
            }
            else
            {
               outList.add(holder.getValue());
            }
         }
      }
      return holders;
   }
}
