package org.jboss.resteasy.core.request;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Variant;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;


/**
 * {@link Variant} selection.
 *
 * @author Pascal S. de Kloe
 * @see "RFC 2296"
 */
public class ServerDrivenNegotiation
{

   private Map<MediaType, QualityValue> requestedMediaTypes = null;
   private Map<String, QualityValue> requestedCharacterSets = null;
   private Map<String, QualityValue> requestedEncodings = null;
   private Map<Locale, QualityValue> requestedLanguages = null;
   private int mediaRadix = 1;


   public ServerDrivenNegotiation()
   {
   }


   public void setAcceptHeaders(List<String> headerValues)
   {
      requestedMediaTypes = null;
      if (headerValues == null)
         return;
      Map<MediaType, QualityValue> requested = null;
      for (String headerValue : headerValues)
      {
         Map<MediaType, QualityValue> mapping = AcceptHeaders.getMediaTypeQualityValues(headerValue);
         if (mapping == null)
            return;
         if (requested == null)
            requested = mapping;
         else
            requested.putAll(mapping);
      }
      requestedMediaTypes = requested;
      for (Iterator<MediaType> it = requested.keySet().iterator(); it.hasNext(); )
      {
         mediaRadix = Math.max(mediaRadix, it.next().getParameters().size());
      }
   }


   public void setAcceptCharsetHeaders(List<String> headerValues)
   {
      requestedCharacterSets = null;
      if (headerValues == null)
         return;
      Map<String, QualityValue> requested = null;
      for (String headerValue : headerValues)
      {
         Map<String, QualityValue> mapping = AcceptHeaders.getStringQualityValues(headerValue);
         if (mapping == null)
            return;
         if (requested == null)
            requested = mapping;
         else
            requested.putAll(mapping);
      }
      requestedCharacterSets = requested;
   }


   public void setAcceptEncodingHeaders(List<String> headerValues)
   {
      requestedEncodings = null;
      if (headerValues == null)
         return;
      Map<String, QualityValue> requested = null;
      for (String headerValue : headerValues)
      {
         Map<String, QualityValue> mapping = AcceptHeaders.getStringQualityValues(headerValue);
         if (mapping == null)
            return;
         if (requested == null)
            requested = mapping;
         else
            requested.putAll(mapping);
      }
      requestedEncodings = requested;
   }


   public void setAcceptLanguageHeaders(List<String> headerValues)
   {
      requestedLanguages = null;
      if (headerValues == null)
         return;
      Map<Locale, QualityValue> requested = null;
      for (String headerValue : headerValues)
      {
         Map<Locale, QualityValue> mapping = AcceptHeaders.getLocaleQualityValues(headerValue);
         if (mapping == null)
            return;
         if (requested == null)
            requested = mapping;
         else
            requested.putAll(mapping);
      }
      requestedLanguages = requested;
   }


   public Variant getBestMatch(List<Variant> available)
   {
//      BigDecimal bestQuality = BigDecimal.ZERO;
      VariantQuality bestQuality = null;
      Variant bestOption = null;
      for (Variant option : available)
      {
         VariantQuality quality = new VariantQuality();
         if (!applyMediaType(option, quality))
            continue;
         if (!applyCharacterSet(option, quality))
            continue;
         if (!applyEncoding(option, quality))
            continue;
         if (!applyLanguage(option, quality))
            continue;

//         BigDecimal optionQuality = quality.getOverallQuality();
//         if (isBetterOption(bestQuality, bestOption, optionQuality, option))
         if (isBetterOption(bestQuality, bestOption, quality, option))
         {
//            bestQuality = optionQuality;
            bestQuality = quality;
            bestOption = option;
         }
      }
      return bestOption;
   }


   /**
    * Tests whether {@code option} is preferable over the current {@code bestOption}.
    */
//   private static boolean isBetterOption(BigDecimal bestQuality, Variant best,
//                                         BigDecimal optionQuality, Variant option)
   private static boolean isBetterOption(VariantQuality bestQuality, Variant best,
                                         VariantQuality optionQuality, Variant option)
   {
      if (best == null)
         return true;
      
      // Compare overall quality.
      int signum = bestQuality.getOverallQuality().compareTo(optionQuality.getOverallQuality());
      if (signum != 0)
         return signum < 0;
      
      // Overall quality is the same.
      // Assuming the request has an Accept header, a VariantQuality has a non-null
      // requestMediaType if and only if it the corresponding Variant has a non-null mediaType.
      // If bestQuality and optionQuality both have a non-null requestMediaType, we compare them
      // for specificity.
      MediaType bestRequestMediaType = bestQuality.getRequestMediaType();
      MediaType optionRequestMediaType = optionQuality.getRequestMediaType();
      if (bestRequestMediaType != null && optionRequestMediaType != null)
      {
         if (bestRequestMediaType.getType().equals(optionRequestMediaType.getType()))
         {
            if (bestRequestMediaType.getSubtype().equals(optionRequestMediaType.getSubtype()))
            {
               int bestCount = bestRequestMediaType.getParameters().size();
               int optionCount = optionRequestMediaType.getParameters().size();
               if (optionCount > bestCount)
               {
                  return true;   // more matching parameters
               }
               else if (optionCount < bestCount)
               {
                  return false;   // less matching parameters
               }
            }
            else if (bestRequestMediaType.getSubtype().equals("*"))
            {
               return true;
            }
            else if (optionRequestMediaType.getSubtype().equals("*"))
            {
               return false;
            }
         }
         else if (bestRequestMediaType.getType().equals("*"))
         {
            return true;
         }
         else if (optionRequestMediaType.getType().equals("*"))
         {
            return false;
         }
      }
      
      // Compare variant media types for specificity.
      MediaType bestType = best.getMediaType();
      MediaType optionType = option.getMediaType();
      if (bestType != null && optionType != null)
      {
         if (bestType.getType().equals(optionType.getType()))
         {
            // Same type
            if (bestType.getSubtype().equals(optionType.getSubtype()))
            {
               // Same subtype
               int bestCount = bestType.getParameters().size();
               int optionCount = optionType.getParameters().size();
               if (optionCount > bestCount)
                  return true;   // more matching parameters
               else if (optionCount < bestCount)
                  return false;   // less matching parameters
            }
            else if ("*".equals(bestType.getSubtype()))
            {
               return true;   // more specific subtype
            }
            else if ("*".equals(optionType.getSubtype()))
            {
               return false;   // less specific subtype
            }
         }
         else if ("*".equals(bestType.getType()))
         {
            return true;   // more specific type
         }
         else if ("*".equals(optionType.getType()))
         {
            return false;   // less specific type;
         }
      }

      // Finally, compare specificity of the variants.
      return getExplicitness(best) < getExplicitness(option);
   }


   private static int getExplicitness(Variant variant)
   {
      int explicitness = 0;
      if (variant.getMediaType() != null)
         ++explicitness;
      if (variant.getEncoding() != null)
         ++explicitness;
      if (variant.getLanguage() != null)
         ++explicitness;
      return explicitness;
   }


   private boolean applyMediaType(Variant option, VariantQuality quality)
   {
      if (requestedMediaTypes == null)
         return true;
      MediaType mediaType = option.getMediaType();
      if (mediaType == null)
         return true;

      String type = mediaType.getType();
      if ("*".equals(type))
         type = null;
      String subtype = mediaType.getSubtype();
      if ("*".equals(subtype))
         subtype = null;
      Map<String, String> parameters = mediaType.getParameters();
      if (parameters.isEmpty())
         parameters = null;

      QualityValue bestQuality = QualityValue.NOT_ACCEPTABLE;
      int bestMatchCount = -1;
      MediaType bestRequestMediaType = null;

      for (MediaType requested : requestedMediaTypes.keySet())
      {
         int matchCount = 0;
         if (type != null)
         {
            String requestedType = requested.getType();
            if (requestedType.equals(type))
               matchCount += mediaRadix * 100;
            else if (!"*".equals(requestedType))
               continue;
         }
         if (subtype != null)
         {
            String requestedSubtype = requested.getSubtype();
            if (requestedSubtype.equals(subtype))
               matchCount += mediaRadix * 10;
            else if (!"*".equals(requestedSubtype))
               continue;
         }
         Map<String, String> requestedParameters = requested.getParameters();
         if (requestedParameters != null && requestedParameters.size() > 0)
         {
            if (!hasRequiredParameters(requestedParameters, parameters))
               continue;
            matchCount += requestedParameters.size();
         }

         if (matchCount > bestMatchCount)
         {
            bestMatchCount = matchCount;
            bestQuality = requestedMediaTypes.get(requested);
            bestRequestMediaType = requested;
         }
         else if (matchCount == bestMatchCount)
         {
            QualityValue qualityValue = requestedMediaTypes.get(requested);
            if (bestQuality.compareTo(qualityValue) < 0)
            {
               bestQuality = qualityValue;
               bestRequestMediaType = requested;
            }
         }
      }

      if (!bestQuality.isAcceptable())
         return false;

      quality.setMediaTypeQualityValue(bestQuality);
      quality.setRequestMediaType(bestRequestMediaType);
      return true;
   }


   private boolean hasRequiredParameters(Map<String, String> required, Map<String, String> available)
   {
      if (available == null)
      {
         return false;
      }
      for (Entry<String, String> requiredEntry : required.entrySet())
      {
         String name = requiredEntry.getKey();
         String value = requiredEntry.getValue();
         String availableValue = available.get(name);
         if (availableValue == null && "charset".equals(name))
         {
            if (requestedCharacterSets != null
                    && !requestedCharacterSets.containsKey(null)
                    && !requestedCharacterSets.containsKey(value))
               return false;
         }
         else if (!value.equals(availableValue))
            return false;
      }
      return true;
   }


   private boolean applyCharacterSet(Variant option, VariantQuality quality)
   {
      if (requestedCharacterSets == null)
         return true;
      MediaType mediaType = option.getMediaType();
      if (mediaType == null)
         return true;
      String charsetParameter = mediaType.getParameters().get("charset");
      if (charsetParameter == null)
         return true;
      QualityValue value = requestedCharacterSets.get(charsetParameter);
      if (value == null)   // try wildcard
         value = requestedCharacterSets.get(null);
      if (value == null)   // no match
         return false;
      if (!value.isAcceptable()) return false;
      quality.setCharacterSetQualityValue(value);
      return true;
   }


   private boolean applyEncoding(Variant option, VariantQuality quality)
   {
      if (requestedEncodings == null)
         return true;
      String encoding = option.getEncoding();
      if (encoding == null)
         return true;
      QualityValue value = requestedEncodings.get(encoding);
      if (value == null)   // try wildcard
         value = requestedEncodings.get(null);
      if (value == null)   // no match
         return false;
      if (!value.isAcceptable()) return false;
      quality.setEncodingQualityValue(value);
      return true;
   }

   private boolean hasCountry(Locale locale)
   {
      return locale.getCountry() != null && !"".equals(locale.getCountry().trim());
   }


   private boolean applyLanguage(Variant option, VariantQuality quality)
   {
      if (requestedLanguages == null)
         return true;
      Locale language = option.getLanguage();
      if (language == null)
         return true;
      QualityValue value = null;
      for (Entry<Locale, QualityValue> entry : requestedLanguages.entrySet())
      {
         Locale locale = entry.getKey();
         QualityValue qualityValue = entry.getValue();
         if (locale == null) continue;

         if (locale.getLanguage().equalsIgnoreCase(language.getLanguage()))
         {
            if (hasCountry(locale) && hasCountry(language))
            {
               if (locale.getCountry().equalsIgnoreCase(language.getCountry()))
               {
                  value = qualityValue;
                  break;
               }
               else
               {
                  continue;
               }
            }
            else if (hasCountry(locale) == hasCountry(language))
            {
               value = qualityValue;
               break;
            }
            else
            {
               value = qualityValue; // might be a better match so re-loop
            }
         }
      }

      if (value == null)   // try wildcard
         value = requestedLanguages.get(null);
      if (value == null)   // no match
         return false;
      if (!value.isAcceptable()) return false;
      quality.setLanguageQualityValue(value);
      return true;
   }

}