package org.jboss.resteasy.core;

import org.jboss.resteasy.plugins.delegates.MediaTypeHeaderDelegate;
import org.jboss.resteasy.util.MediaTypeHelper;

import javax.ws.rs.core.MediaType;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A copy-on-write MediaType index of T.
 *
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class MediaTypeMap<T>
{
   public interface Typed
   {
      Class<?> getType();
   }

   private static class TypedEntryComparator implements Comparator<Entry<?>>, Serializable
   {
      private static final long serialVersionUID = -8815419198743440920L;
      private Class<?> type;

      TypedEntryComparator(final Class<?> type)
      {
         this.type = type;
      }

      private boolean isAssignableFrom(Typed typed)
      {
         if (typed.getType() == null) return false;
         return typed.getType().isAssignableFrom(type);
      }

      private int compareTypes(Entry<?> entry, Entry<?> entry1)
      {
         int val = 0;
         if (entry.object instanceof Typed && entry1.object instanceof Typed && type != null)
         {
            Typed one = (Typed) entry.object;
            Typed two = (Typed) entry1.object;


            boolean oneTyped = isAssignableFrom(one);
            boolean twoTyped = isAssignableFrom(two);
            if (oneTyped == twoTyped && (!oneTyped && !twoTyped))
            {
               // both are false
               val = 0;
            }
            else if (oneTyped == twoTyped)
            {
               // both are true.
               // test for better assignability
               if (one.getType().equals(two.getType()))
               {
                  val = 0;
               }
               else if (one.getType().isAssignableFrom(two.getType()))
               {
                  val = 1;
               }
               else
               {
                  val = -1;
               }
            }
            else if (oneTyped) val = -1;
            else val = 1;
         }
         return val;

      }

      public int compare(Entry<?> entry, Entry<?> entry1)
      {
         int val = compareTypes(entry, entry1);
         if (val == 0) val = entry.compareTo(entry1);
         return val;
      }
   }

   private static class Entry<T> implements Comparable<Entry<?>>
   {
      public MediaType mediaType;
      public T object;

      private Entry(final MediaType mediaType, final T object)
      {
         this.mediaType = mediaType;
         this.object = object;
      }

      @SuppressWarnings({"rawtypes", "unchecked"})
      public int compareTo(Entry<?> entry)
      {
         int val = MediaTypeHelper.compareWeight(mediaType, entry.mediaType);
         if (val == 0 && object instanceof Comparable && entry.object instanceof Comparable)
         {
            return ((Comparable) object).compareTo(entry.object);
         }
         return val;
      }
   }

   private static final Pattern COMPOSITE_PATTERN = Pattern.compile("([^\\+]+)\\+(.+)");

   // Composite subtypes are of the pattern *+subtype i.e. *+xml, *+json
   public static final Pattern COMPOSITE_SUBTYPE_WILDCARD_PATTERN = Pattern.compile("\\*\\+(.+)");


   // This composite is subtype+*  i.e. atom+* rss+*
   public static final Pattern WILD_SUBTYPE_COMPOSITE_PATTERN = Pattern.compile("([^\\+]+)\\+\\*");

   public static String compositeWildSubtype(String subtype) {
      final Matcher matcher = COMPOSITE_SUBTYPE_WILDCARD_PATTERN.matcher(subtype);
      if (matcher.matches()) {
         return matcher.group(1);
      }
      return null;
   }

   public static String wildCompositeSubtype(String subtype) {
      final Matcher matcher = WILD_SUBTYPE_COMPOSITE_PATTERN.matcher(subtype);
      if (matcher.matches()) {
         return matcher.group(1);
      }
      return null;
   }

   private class SubtypeMap<T>
   {
      private volatile Map<String, List<Entry<T>>> index;
      private volatile Map<String, List<Entry<T>>> compositeIndex;
      private volatile Map<String, List<Entry<T>>> wildCompositeIndex;
      private volatile List<Entry<T>> wildcards;
      private volatile List<Entry<T>> all;

      private SubtypeMap() {
         index = new HashMap<>();
         compositeIndex = new HashMap<>();
         wildCompositeIndex = new HashMap<>();
         wildcards = new ArrayList<>();
         all = new ArrayList<>();
      }

      private SubtypeMap(final SubtypeMap<T> subtypeMap) {
         index = subtypeMap.index;
         compositeIndex = subtypeMap.compositeIndex;
         wildCompositeIndex = subtypeMap.wildCompositeIndex;
         wildcards = subtypeMap.wildcards;
         all = subtypeMap.all;
      }


      private void add(Entry<T> entry)
      {
         final Matcher matcher = COMPOSITE_SUBTYPE_WILDCARD_PATTERN.matcher(entry.mediaType.getSubtype());
         final Matcher wildCompositeMatcher = WILD_SUBTYPE_COMPOSITE_PATTERN.matcher(entry.mediaType.getSubtype());


         if (entry.mediaType.isWildcardSubtype()) {
            addWildcard(entry);
         }
         else if (matcher.matches())
         {
            String baseSubType = matcher.group(1);
            addCompositeWild(entry, baseSubType);
         }
         else if (wildCompositeMatcher.matches())
         {
            String base = wildCompositeMatcher.group(1);
            addWildComposite(entry, base);
         }
         else
         {
            addRegular(entry);
         }
      }

      private void addRegular(Entry<T> entry) {
         Map<String, List<Entry<T>>> newIndex = index;
         if (lockSnapshots) newIndex = copy(index);
         add(newIndex, entry.mediaType.getSubtype(), entry);
         index = newIndex;
         merge(entry);
      }

      private void addWildComposite(Entry<T> entry, String base) {
         Map<String, List<Entry<T>>> newWildCompositeIndex = wildCompositeIndex;
         if (lockSnapshots) newWildCompositeIndex = (wildCompositeIndex);
         add(newWildCompositeIndex, base, entry);
         wildCompositeIndex = newWildCompositeIndex;
         merge(entry);
      }

      private void addCompositeWild(Entry<T> entry, String baseSubType) {
         Map<String, List<Entry<T>>> newCompositeIndex = compositeIndex;
         if (lockSnapshots) newCompositeIndex = copy(compositeIndex);
         add(newCompositeIndex, baseSubType, entry);
         compositeIndex = newCompositeIndex;
         merge(entry);
      }

      private void addWildcard(Entry<T> entry) {
         if (lockSnapshots) wildcards = copyAndAdd(wildcards, entry);
         else wildcards.add(entry);
         merge(entry);
      }

      private void merge(Entry<T> entry) {
         if (lockSnapshots) {
            all = copyAndAdd(all, entry);
         } else {
            all.add(entry);
         }
      }

      private Map<String, List<Entry<T>>> copy(final Map<String, List<Entry<T>>> original) {
         final Map<String, List<Entry<T>>> copy = new HashMap<>(original.size());
         original.forEach((key, value) -> copy.put(key, new ArrayList<>(value)));
         return copy;
      }

      private void add(final Map<String, List<Entry<T>>> map,
                       final String key,
                       final Entry<T> entry) {
         map.putIfAbsent(key, new CopyOnWriteArrayList<>());
         map.get(key).add(entry);
      }

      private List<Entry<T>> getPossible(MediaType accept)
      {
         if (accept.isWildcardSubtype())
         {
            return all;
         }
         else
         {
            List<Entry<T>> matches = new ArrayList<Entry<T>>();

            List<Entry<T>> indexed = index.get(accept.getSubtype());
            if (indexed != null) matches.addAll(indexed);

            Matcher matcher = COMPOSITE_PATTERN.matcher(accept.getSubtype());
            String compositeKey = accept.getSubtype();
            if (matcher.matches())
            {
               String wildCompositeKey = matcher.group(1);
               List<Entry<T>> windex = wildCompositeIndex.get(wildCompositeKey);
               if (windex != null) matches.addAll(windex);
               compositeKey = matcher.group(2);
            }
            List<Entry<T>> indexed2 = compositeIndex.get(compositeKey);
            if (indexed2 != null) matches.addAll(indexed2);
            matches.addAll(wildcards);
            return matches;
         }
      }
   }

   static <A> List<A> copyAndAdd(List<A> a, A entry) {
      // reduce internal array copying
      ArrayList<A> newList = new ArrayList<A>(a.size() + 1);
      newList.add(entry);
      newList.addAll(0, a);
      return newList;
   }

   private static class CachedMediaTypeAndClass
   {
      // we need a weak reference because of possible hot deployment
      // Although, these reference should get cleared up with any add() invocation
      private WeakReference<Class<?>> clazz;
      private MediaType mediaType;
      private final int hash;

      @SuppressWarnings({"rawtypes", "unchecked"})
      private CachedMediaTypeAndClass(final Class clazz, final MediaType mediaType)
      {
         this.clazz = new WeakReference(clazz);
         this.mediaType = mediaType;
         int result = clazz.hashCode();
         result = 31 * result + (mediaType.getType() != null ? mediaType.getType().hashCode() : 0) +  (mediaType.getSubtype() != null ? mediaType.getSubtype().hashCode() : 0);
         hash = result;
      }

      private Class<?> getClazz()
      {
         return clazz.get();
      }

      @Override
      public boolean equals(Object o)
      {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;

         CachedMediaTypeAndClass that = (CachedMediaTypeAndClass) o;

         // WeakReference may have GC'd
         Class<?> clazz = getClazz();
         if (clazz == null || that.getClazz() == null) return false;

         if (!clazz.equals(that.getClazz())) return false;

         if (mediaType.getType() != null)
         {
            if (!mediaType.getType().equals(that.mediaType.getType())) return false;
         }
         else if ((mediaType.getType() != that.mediaType.getType())) return false;

         if (mediaType.getSubtype() != null)
         {
            if (!mediaType.getSubtype().equals(that.mediaType.getSubtype())) return false;
         }
         else if ((mediaType.getSubtype() != that.mediaType.getSubtype())) return false;

         return true;
      }

      @Override
      public int hashCode()
      {
         return hash;
      }
   }

   private volatile Map<String, SubtypeMap<T>> index;
   private volatile Map<CachedMediaTypeAndClass, List<T>> classCache;
   private volatile List<Entry<T>> wildcards;
   private volatile List<Entry<T>> everything;
   private boolean lockSnapshots;

   public MediaTypeMap() {
      index = new HashMap<>();
      wildcards = new ArrayList<>();
      everything = new ArrayList<>();
   }

   /**
    * Shallow copy, any additional adds will deep copy
    *
    * @param mediaTypeMap
    */
   public MediaTypeMap(final MediaTypeMap<T> mediaTypeMap) {
      lockSnapshots = true;
      index      = mediaTypeMap.index;
      wildcards  = mediaTypeMap.wildcards;
      everything        = mediaTypeMap.everything;
      classCache = mediaTypeMap.classCache;
   }

   /**
    * After this is called, all new adds will deep copy itself.
    *
    */
   public void lockSnapshots() {
      //if (!lockSnapshots) Collections.sort(everything);
      lockSnapshots = true;
   }

   /**
    * Add an object to the media type map.  This is synchronized to serialize adds.
    *
    * @param type media type
    * @param obj object
    */
   public synchronized void add(final MediaType type, final T obj)
   {
      final MediaType newType =
              new MediaType(type.getType().toLowerCase(), type.getSubtype().toLowerCase(), type.getParameters());
      final Entry<T> entry = new Entry<>(newType, obj);

      add(entry);
   }

   /**
    * Add an object to the media type map.  This is synchronized to serialize adds.
    *
    * @param mediaType media type
    * @param obj object
    */
   public synchronized void add(String mediaType, final T obj)
   {
      final MediaType newType = MediaTypeHeaderDelegate.parse(mediaType.toLowerCase());
      final Entry<T> entry = new Entry<>(newType, obj);
      add(entry);
   }

   public synchronized void addWildcard(final T obj) {
      final Entry<T> entry = new Entry<>(MediaType.WILDCARD_TYPE, obj);
      addWildcard(entry);
   }


   protected void add(Entry<T> entry) {
      if (entry.mediaType.isWildcardType())
      {
         addWildcard(entry);
      }
      else
      {
         Map<String, SubtypeMap<T>> newIndex = copyIndex();
         newIndex.putIfAbsent(entry.mediaType.getType(), new SubtypeMap<>());
         newIndex.get(entry.mediaType.getType()).add(entry);
         index = newIndex;
         mergeEverything(entry);
      }
   }

   private Map<String, SubtypeMap<T>> copyIndex() {
      Map<String, SubtypeMap<T>> newIndex = index;
      if (lockSnapshots) {
         Map<String, SubtypeMap<T>> finalIndex = new HashMap<>();
         newIndex = finalIndex;
         index.forEach((key, value) -> finalIndex.put(key, new SubtypeMap<>(value)));
      }
      return newIndex;
   }

   private void addWildcard(Entry<T> entry) {
      if (lockSnapshots) wildcards = copyAndAdd(wildcards, entry);
      else wildcards.add(entry);
      mergeEverything(entry);
   }

   public synchronized void addRegular(MediaType mediaType, T obj) {
      final Entry<T> entry = new Entry<>(mediaType, obj);
      Map<String, SubtypeMap<T>> newIndex = copyIndex();
      newIndex.putIfAbsent(entry.mediaType.getType(), new SubtypeMap<>());
      SubtypeMap<T> subtypeMap = newIndex.get(entry.mediaType.getType());
      subtypeMap.addRegular(entry);
      index = newIndex;
      mergeEverything(entry);
   }

   public synchronized void addCompositeWild(MediaType mediaType, T obj, String baseSubtype) {
      final Entry<T> entry = new Entry<>(mediaType, obj);
      Map<String, SubtypeMap<T>> newIndex = copyIndex();
      newIndex.putIfAbsent(entry.mediaType.getType(), new SubtypeMap<>());
      SubtypeMap<T> subtypeMap = newIndex.get(entry.mediaType.getType());

      subtypeMap.addCompositeWild(entry, baseSubtype);

      index = newIndex;
      mergeEverything(entry);
   }

   public synchronized void addWildComposite(MediaType mediaType, T obj, String baseSubtype) {
      final Entry<T> entry = new Entry<>(mediaType, obj);
      Map<String, SubtypeMap<T>> newIndex = copyIndex();
      newIndex.putIfAbsent(entry.mediaType.getType(), new SubtypeMap<>());
      SubtypeMap<T> subtypeMap = newIndex.get(entry.mediaType.getType());

      subtypeMap.addWildComposite(entry, baseSubtype);

      index = newIndex;
      mergeEverything(entry);
   }

   public synchronized void addWildSubtype(MediaType mediaType, T obj) {
      final Entry<T> entry = new Entry<>(mediaType, obj);
      Map<String, SubtypeMap<T>> newIndex = copyIndex();
      newIndex.putIfAbsent(entry.mediaType.getType(), new SubtypeMap<>());
      SubtypeMap<T> subtypeMap = newIndex.get(entry.mediaType.getType());

      subtypeMap.addWildcard(entry);

      index = newIndex;
      mergeEverything(entry);
   }

   private void mergeEverything(Entry<T> entry) {
      List<Entry<T>> newAll = everything;
      if (lockSnapshots) {
         newAll = copyAndAdd(everything, entry);
         Collections.sort(newAll);
         everything = newAll;
      } else {
         everything.add(entry);
         Collections.sort(everything);
      }
      classCache = null;
   }


   private static <T> List<T> convert(List<Entry<T>> list)
   {
      List<T> newList = new ArrayList<T>(list.size());
      for (Entry<T> entry : list)
      {
         newList.add(entry.object);
      }
      return newList;

   }

   /**
    * Returns a list of objects sorted based on their media type where the first in the list
    * is the best match
    *
    * @param accept mime to match
    * @return list of objects
    */
   public List<T> getPossible(MediaType accept)
   {
      accept = new MediaType(accept.getType().toLowerCase(), accept.getSubtype().toLowerCase(), accept.getParameters());
      List<Entry<T>> matches = new ArrayList<Entry<T>>();
      if (accept.isWildcardType())
      {
         return convert(everything);
      }
      else
      {
         SubtypeMap<T> indexed = index.get(accept.getType());
         if (indexed != null)
         {
            matches.addAll(indexed.getPossible(accept));
         }
         matches.addAll(wildcards);
      }
      Collections.sort(matches);
      return convert(matches);
   }

   /**
    * By default, MediaTypeMap will cache possible MediaType/Class matches.  Set this to false to turn off
    * caching
    *
    */
   public static boolean useCache = true;

   public List<T> getPossible(MediaType accept, Class<?> type)
   {
      List<T> cached = null;
      CachedMediaTypeAndClass cacheEntry = null;
      if (useCache)
      {
         cacheEntry = new CachedMediaTypeAndClass(type, accept);
         if (classCache != null) {
            cached = classCache.get(cacheEntry);
            if (cached != null) return cached;
         }
      }

      accept = new MediaType(accept.getType().toLowerCase(), accept.getSubtype().toLowerCase(), accept.getParameters());
      List<Entry<T>> matches = new ArrayList<Entry<T>>();
      if (accept.isWildcardType())
      {
         matches.addAll(everything);
      }
      else
      {
         SubtypeMap<T> indexed = index.get(accept.getType());
         if (indexed != null)
         {
            matches.addAll(indexed.getPossible(accept));
         }
         matches.addAll(wildcards);
      }
      Collections.sort(matches, new TypedEntryComparator(type));
      cached = convert(matches);
      if (useCache) {
         Map<CachedMediaTypeAndClass, List<T>> cache = classCache;
         if (cache == null) {
            synchronized (this)
            {
               if (classCache == null)
               {
                  classCache = new HashMap<>();
               }
               cache = classCache;
            }
         }
         cache.put(cacheEntry, cached);
      }
      return cached;

   }

}
