package org.jboss.resteasy.util;

import org.jboss.resteasy.resteasy_jaxrs.i18n.Messages;
import org.jboss.resteasy.spi.LoggableFailure;

import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import java.io.Serializable;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class MediaTypeHelper
{
   private static final MediaTypeComparator COMPARATOR = new MediaTypeComparator();

   @SuppressWarnings(value = "unchecked")
   public static MediaType getConsumes(Class declaring, AccessibleObject method)
   {
      Consumes consume = method.getAnnotation(Consumes.class);
      if (consume == null)
      {
         consume = (Consumes) declaring.getAnnotation(Consumes.class);
         if (consume == null) return null;
      }
      return MediaType.valueOf(consume.value()[0]);
   }

   public static MediaType getProduces(Class declaring, Method method)
   {
      return getProduces(declaring, method, null);
   }

   @SuppressWarnings(value = "unchecked")
   public static MediaType getProduces(Class declaring, Method method, MediaType defaultProduces)
   {
      Produces consume = method.getAnnotation(Produces.class);
      if (consume == null)
      {
         consume = (Produces) declaring.getAnnotation(Produces.class);
      }
      if (consume == null) return defaultProduces;
      return MediaType.valueOf(consume.value()[0]);
   }

   public static float getQ(MediaType type)
   {
      float rtn = getQWithParamInfo(type);
      if (rtn == 2.0F) return 1.0F;
      return rtn;
   }

   public static float getQWithParamInfo(MediaType type)
   {
      if (type.getParameters() != null)
      {
         String val = type.getParameters().get("q");
         try
         {
            if (val != null)
            {
               float rtn = Float.valueOf(val);
               if (rtn > 1.0F)
                  throw new LoggableFailure(Messages.MESSAGES.mediaTypeQGreaterThan1(type.toString()), HttpResponseCodes.SC_BAD_REQUEST);
               return rtn;
            }
         }
         catch (NumberFormatException e)
         {
            throw new RuntimeException(Messages.MESSAGES.mediaTypeQMustBeFloat(type), e);
         }
      }
      return 2.0f;
   }

   /**
    * subtypes like application/*+xml
    *
    * @param subtype subtype
    * @return true if subtype is composite
    */
   public static boolean isCompositeWildcardSubtype(String subtype)
   {
      return subtype.startsWith("*+");
   }

   /**
    * subtypes like application/*+xml
    *
    * @param subtype subtype
    * @return true if subtype is wildcard composite
    */
   public static boolean isWildcardCompositeSubtype(String subtype)
   {
      return subtype.endsWith("+*");
   }

   public static boolean isComposite(String subtype)
   {
      return (isCompositeWildcardSubtype(subtype) || isWildcardCompositeSubtype(subtype));
   }

   private static class MediaTypeComparator implements Comparator<MediaType>, Serializable
   {

      private static final long serialVersionUID = -5828700121582498092L;

      public int compare(MediaType mediaType2, MediaType mediaType)
      {
         float q = getQWithParamInfo(mediaType);
         boolean wasQ = q != 2.0f;
         if (q == 2.0f) q = 1.0f;

         float q2 = getQWithParamInfo(mediaType2);
         boolean wasQ2 = q2 != 2.0f;
         if (q2 == 2.0f) q2 = 1.0f;


         if (q < q2) return -1;
         if (q > q2) return 1;

         if (mediaType.isWildcardType() && !mediaType2.isWildcardType()) return -1;
         if (!mediaType.isWildcardType() && mediaType2.isWildcardType()) return 1;
         if (mediaType.isWildcardSubtype() && !mediaType2.isWildcardSubtype()) return -1;
         if (!mediaType.isWildcardSubtype() && mediaType2.isWildcardSubtype()) return 1;
         if (isComposite(mediaType.getSubtype()) && !isComposite(mediaType2.getSubtype()))
            return -1;
         if (!isComposite(mediaType.getSubtype()) && isComposite(mediaType2.getSubtype()))
            return 1;
         if (isCompositeWildcardSubtype(mediaType.getSubtype()) && !isCompositeWildcardSubtype(mediaType2.getSubtype()))
            return -1;
         if (!isCompositeWildcardSubtype(mediaType.getSubtype()) && isCompositeWildcardSubtype(mediaType2.getSubtype()))
            return 1;
         if (isWildcardCompositeSubtype(mediaType.getSubtype()) && !isWildcardCompositeSubtype(mediaType2.getSubtype()))
            return -1;
         if (!isWildcardCompositeSubtype(mediaType.getSubtype()) && isWildcardCompositeSubtype(mediaType2.getSubtype()))
            return 1;

         int numNonQ = 0;
         if (mediaType.getParameters() != null)
         {
            numNonQ = mediaType.getParameters().size();
            if (wasQ) numNonQ--;
         }

         int numNonQ2 = 0;
         if (mediaType2.getParameters() != null)
         {
            numNonQ2 = mediaType2.getParameters().size();
            if (wasQ2) numNonQ2--;
         }

         if (numNonQ < numNonQ2) return -1;
         if (numNonQ > numNonQ2) return 1;


         return 0;
      }
   }

   public static int compareWeight(MediaType one, MediaType two)
   {
      return COMPARATOR.compare(one, two);
   }

   public static boolean sameWeight(MediaType one, MediaType two)
   {
      return COMPARATOR.compare(one, two) == 0;
   }

   public static void sortByWeight(List<MediaType> types)
   {
      if (types == null || types.size() <= 1) return;
      Collections.sort(types, COMPARATOR);
   }

   public static MediaType getBestMatch(List<MediaType> desired, List<MediaType> provided)
   {
      sortByWeight(desired);
      sortByWeight(provided);
      boolean emptyDesired = desired == null || desired.size() == 0;
      boolean emptyProvided = provided == null || provided.size() == 0;

      if (emptyDesired && emptyProvided) return null;
      if (emptyDesired && !emptyProvided) return provided.get(0);
      if (emptyProvided && !emptyDesired) return desired.get(0);

      for (MediaType desire : desired)
      {
         for (MediaType provide : provided)
         {
            if (provide.isCompatible(desire)) return provide;
         }
      }
      return null;
   }

   public static List<MediaType> parseHeader(String header)
   {
      ArrayList<MediaType> types = new ArrayList<MediaType>();
      String[] medias = header.split(",");
      for (int i = 0; i < medias.length; i++)
      {
         types.add(MediaType.valueOf(medias[i].trim()));
      }
      return types;
   }

   public static boolean equivalent(MediaType m1, MediaType m2)
   {
      if (m1 == m2) return true;

      if (!m1.getType().equals(m2.getType())) return false;
      if (!m1.getSubtype().equals(m2.getSubtype())) return false;

      return equivalentParams(m1, m2);
   }

   public static boolean equivalentParams(MediaType m1, MediaType m2)
   {
      Map<String, String> params1 = m1.getParameters();
      Map<String, String> params2 = m2.getParameters();

      if (params1 == params2) return true;
      if (params1 == null || params2 == null) return false;
      if (params1.size() == 0 && params2.size() == 0) return true;
      int numParams1 = params1.size();
      if (params1.containsKey("q")) numParams1--;
      int numParams2 = params2.size();
      if (params2.containsKey("q")) numParams2--;

      if (numParams1 != numParams2) return false;
      if (numParams1 == 0) return true;

      for (Map.Entry<String, String> entry : params1.entrySet())
      {
         String key = entry.getKey();
         if (key.equals("q")) continue;
         String value = entry.getValue();
         String value2 = params2.get(key);
         if (value == value2) continue; // both null
         if (value == null || value2 == null) return false;
         if (value.equals(value2) == false) return false;
      }
      return true;
   }

   public static boolean isTextLike(MediaType mediaType)
   {
      return "text".equalsIgnoreCase(mediaType.getType())
            || ("application".equalsIgnoreCase(mediaType.getType())
                  && mediaType.getSubtype().toLowerCase().startsWith("xml"));
   }

   public static boolean isBlacklisted(MediaType mediaType)
   {
      return "application".equals(mediaType.getType()) && "signed-exchange".equals(mediaType.getSubtype());
   }
}
