/*
 * This file is part of the Meeds project (https://meeds.io/).
 * Copyright (C) 2020 Meeds Association
 * contact@meeds.io
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.exoplatform.services.rest.impl;

import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.rest.GenericContainerRequest;
import org.exoplatform.services.rest.impl.header.AcceptLanguage;
import org.exoplatform.services.rest.impl.header.AcceptMediaType;
import org.exoplatform.services.rest.impl.header.HeaderHelper;
import org.exoplatform.services.rest.impl.header.Language;

import java.io.InputStream;
import java.net.URI;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.*;
import javax.ws.rs.core.Response.ResponseBuilder;

/**
 * @author <a href="mailto:andrew00x@gmail.com">Andrey Parfonov</a>
 * @version $Id: $
 */
public class ContainerRequest implements GenericContainerRequest
{

   /**
    * HTTP method.
    */
   private String method;

   /**
    * HTTP request message body as stream.
    */
   private InputStream entityStream;

   /**
    * HTTP headers.
    */
   private MultivaluedMap<String, String> httpHeaders;

   /**
    * Parsed HTTP cookies.
    */
   private Map<String, Cookie> cookies;

   /**
    * Source strings of HTTP cookies.
    */
   private List<String> cookieHeaders;

   /**
    * HTTP header Content-Type.
    */
   private MediaType contentType;

   /**
    * HTTP header Content-Language.
    */
   private Locale contentLanguage;

   /**
    * List of accepted media type, HTTP header Accept. List is sorted by quality
    * value factor.
    */
   private List<MediaType> acceptMediaType;

   /**
    * List of accepted language, HTTP header Accept-Language. List is sorted by
    * quality value factor.
    */
   private List<Locale> acceptLanguage;

   /**
    * Full request URI, includes query string and fragment.
    */
   private URI requestUri;

   /**
    * Base URI, e.g. servlet path.  
    */
   private URI baseUri;

   private static final Log LOG = ExoLogger.getLogger("exo.ws.rest.core.ContainerRequest");

   /**
    * Constructs new instance of ContainerRequest.
    * 
    * @param method HTTP method
    * @param requestUri full request URI
    * @param baseUri base request URI
    * @param entityStream request message body as stream
    * @param httpHeaders HTTP headers
    */
   public ContainerRequest(String method, URI requestUri, URI baseUri, InputStream entityStream,
      MultivaluedMap<String, String> httpHeaders)
   {
      this.method = method;
      this.requestUri = requestUri;
      this.baseUri = baseUri;
      this.entityStream = entityStream;
      this.httpHeaders = httpHeaders;
   }

   // GenericContainerRequest

   /**
    * {@inheritDoc}
    */
   public MediaType getAcceptableMediaType(List<MediaType> mediaTypes)
   {
      if (mediaTypes.isEmpty())
         // getAcceptableMediaTypes() return list which contains at least one
         // element even HTTP header 'accept' is absent
         return getAcceptableMediaTypes().get(0);

      List<MediaType> l = getAcceptableMediaTypes();

      for (MediaType at : l)
      {
         if (at.isWildcardType())
            // any media type from given list is acceptable the take first
            return mediaTypes.get(0);

         for (MediaType rt : mediaTypes)
         {
            // skip all media types if it has wildcard at type or sub-type
            if (rt.isCompatible(at) && !rt.isWildcardType() && !rt.isWildcardSubtype())
               return rt;
         }
      }

      return null;
   }

   /**
    * {@inheritDoc}
    */
   public List<String> getCookieHeaders()
   {
      if (cookieHeaders == null)
      {
         List<String> c = getRequestHeader(COOKIE);
         if (c != null && c.size() > 0)
            cookieHeaders = Collections.unmodifiableList(getRequestHeader(COOKIE));
         else
            cookieHeaders = Collections.emptyList();
      }
      return cookieHeaders;
   }

   /**
    * {@inheritDoc}
    */
   public InputStream getEntityStream()
   {
      return entityStream;
   }

   /**
    * {@inheritDoc}
    */
   public URI getRequestUri()
   {
      return requestUri;
   }

   /**
    * {@inheritDoc}
    */
   public URI getBaseUri()
   {
      return baseUri;
   }

   /**
    * {@inheritDoc}
    */
   public void setMethod(String method)
   {
      this.method = method;
   }

   /**
    * {@inheritDoc}
    */
   public void setEntityStream(InputStream entityStream)
   {
      this.entityStream = entityStream;

      // reset form data, it should be recreated
      ApplicationContextImpl.getCurrent().getAttributes().remove("org.exoplatform.ws.rs.entity.form");
   }

   /**
    * {@inheritDoc}
    */
   public void setUris(URI requestUri, URI baseUri)
   {
      this.requestUri = requestUri;
      this.baseUri = baseUri;
   }

   /**
    * {@inheritDoc}
    */
   public void setCookieHeaders(List<String> cookieHeaders)
   {
      this.cookieHeaders = cookieHeaders;

      // reset parsed cookies
      this.cookies = null;
   }

   /**
    * {@inheritDoc}
    */
   public void setRequestHeaders(MultivaluedMap<String, String> httpHeaders)
   {
      this.httpHeaders = httpHeaders;

      // reset dependent fields
      this.cookieHeaders = null;
      this.cookies = null;
      this.contentType = null;
      this.contentLanguage = null;
      this.acceptMediaType = null;
      this.acceptLanguage = null;
   }

   // javax.ws.rs.core.SecurityContext

   // Methods from SecurityContext will have different implementation for
   // different container. Currently thinking about servlet container only but
   // for flexible don't implement it here, it must be implemented in super
   // classes.
   /**
    * {@inheritDoc}
    */
   public String getAuthenticationScheme()
   {
      throw new UnsupportedOperationException();
   }

   /**
    * {@inheritDoc}
    */
   public Principal getUserPrincipal()
   {
      throw new UnsupportedOperationException();
   }

   /**
    * {@inheritDoc}
    */
   public boolean isSecure()
   {
      throw new UnsupportedOperationException();
   }

   /**
    * {@inheritDoc}
    */
   public boolean isUserInRole(String role)
   {
      throw new UnsupportedOperationException();
   }

   // javax.ws.rs.core.Request

   /**
    * {@inheritDoc}
    */
   public ResponseBuilder evaluatePreconditions(EntityTag etag)
   {
      ResponseBuilder rb = evaluateIfMatch(etag);
      if (rb != null)
         return rb;

      return evaluateIfNoneMatch(etag);
   }

   /**
    * {@inheritDoc}
    */
   public ResponseBuilder evaluatePreconditions(Date lastModified)
   {
      long lastModifiedTime = lastModified.getTime();
      ResponseBuilder rb = evaluateIfModified(lastModifiedTime);
      if (rb != null)
         return rb;

      return evaluateIfUnmodified(lastModifiedTime);

   }

   /**
    * {@inheritDoc}
    */
   public ResponseBuilder evaluatePreconditions(Date lastModified, EntityTag etag)
   {
      ResponseBuilder rb = evaluateIfMatch(etag);
      if (rb != null)
         return rb;

      long lastModifiedTime = lastModified.getTime();
      rb = evaluateIfModified(lastModifiedTime);
      if (rb != null)
         return rb;

      rb = evaluateIfNoneMatch(etag);
      if (rb != null)
         return rb;

      return evaluateIfUnmodified(lastModifiedTime);

   }

   /**
    * {@inheritDoc}
    */
   public String getMethod()
   {
      return method;
   }

   /**
    * {@inheritDoc}
    */
   public Variant selectVariant(List<Variant> variants)
   {
      if (variants == null || variants.isEmpty())
      {
         throw new IllegalArgumentException("The list of variants is null or empty");
      }
      // Response will be set in RequestDispatcher if set Response
      // now then it will be any way rewrite in RequestDispatcher.
      return VariantsHandler.handleVariants(this, variants);
   }

   // javax.ws.rs.core.HttpHeaders

   /**
    * If accept-language header does not present or its length is null then
    * default language list will be returned. This list contains only one element
    * Locale with language '*', and it minds any language accepted. {@inheritDoc}
    */
   public List<Locale> getAcceptableLanguages()
   {
      if (acceptLanguage == null)
      {
         List<AcceptLanguage> l =
            HeaderHelper.createAcceptedLanguageList(HeaderHelper.convertToString(getRequestHeader(ACCEPT_LANGUAGE)));
         List<Locale> t = new ArrayList<Locale>(l.size());
         // extract Locales from AcceptLanguage
         for (AcceptLanguage al : l)
            t.add(al.getLocale());

         acceptLanguage = Collections.unmodifiableList(t);
      }

      return acceptLanguage;
   }

   /**
    * If accept header does not presents or its length is null then list with one
    * element will be returned. That one element is default media type, see
    * {@link AcceptMediaType#DEFAULT} . {@inheritDoc}
    */
   public List<MediaType> getAcceptableMediaTypes()
   {
      if (acceptMediaType == null)
      {
         // 'extract' MediaType from AcceptMediaType
         List<MediaType> t =
            new ArrayList<MediaType>(HeaderHelper.createAcceptedMediaTypeList(HeaderHelper
               .convertToString(getRequestHeader(ACCEPT))));
         acceptMediaType = Collections.unmodifiableList(t);
      }

      return acceptMediaType;
   }

   /**
    * {@inheritDoc}
    */
   public Map<String, Cookie> getCookies()
   {
      if (cookies == null)
      {
         Map<String, Cookie> t = new HashMap<String, Cookie>();

         for (String ch : getCookieHeaders())
         {
            List<Cookie> l = HeaderHelper.parseCookies(ch);
            for (Cookie c : l)
               t.put(c.getName(), c);
         }

         cookies = Collections.unmodifiableMap(t);
      }

      return cookies;
   }

   /**
    * {@inheritDoc}
    */
   public Locale getLanguage()
   {
      if (contentLanguage == null && httpHeaders.getFirst(CONTENT_LANGUAGE) != null)
      {
         contentLanguage = Language.getLocale(httpHeaders.getFirst(CONTENT_LANGUAGE));
      }

      return contentLanguage;
   }

   /**
    * {@inheritDoc}
    */
   public MediaType getMediaType()
   {
      if (contentType == null && httpHeaders.getFirst(CONTENT_TYPE) != null)
      {
         contentType = MediaType.valueOf(httpHeaders.getFirst(CONTENT_TYPE));
      }

      return contentType;
   }

   /**
    * {@inheritDoc}
    */
   public List<String> getRequestHeader(String name)
   {
      return httpHeaders.get(name);
   }

   /**
    * {@inheritDoc}
    */
   public MultivaluedMap<String, String> getRequestHeaders()
   {
      return httpHeaders;
   }

   /**
    * Comparison for If-Match header and ETag.
    * 
    * @param etag the ETag
    * @return ResponseBuilder with status 412 (precondition failed) if If-Match
    *         header is NOT MATCH to ETag or null otherwise
    */
   private ResponseBuilder evaluateIfMatch(EntityTag etag)
   {
      String ifMatch = getRequestHeaders().getFirst(IF_MATCH);
      // Strong comparison is required.
      // From specification:
      // The strong comparison function: in order to be considered equal,
      // both validators MUST be identical in every way, and both MUST
      // NOT be weak.

      if (ifMatch == null)
         return null;

      EntityTag otherEtag = EntityTag.valueOf(ifMatch);

      if ((etag.isWeak() || otherEtag.isWeak()) // one of tag is weak
         || (!"*".equals(otherEtag.getValue()) && !etag.getValue().equals(otherEtag.getValue())))
      {
         return Response.status(Response.Status.PRECONDITION_FAILED);
      }

      // if tags are not matched then do as tag 'if-match' is absent
      return null;

   }

   /**
    * Comparison for If-None-Match header and ETag.
    * 
    * @param etag the ETag
    * @return ResponseBuilder with status 412 (precondition failed) if
    *         If-None-Match header is MATCH to ETag and HTTP method is not GET or
    *         HEAD. If method is GET or HEAD and If-None-Match is MATCH to ETag
    *         then ResponseBuilder with status 304 (not modified) will be
    *         returned.
    */
   private ResponseBuilder evaluateIfNoneMatch(EntityTag etag)
   {
      String ifNoneMatch = getRequestHeaders().getFirst(IF_NONE_MATCH);

      if (ifNoneMatch == null)
         return null;

      EntityTag otherEtag = EntityTag.valueOf(ifNoneMatch);
      String httpMethod = getMethod();
      // The weak comparison function can only be used with GET or HEAD requests.
      if (httpMethod.equals(HttpMethod.GET) || httpMethod.equals(HttpMethod.HEAD))
      {

         if ("*".equals(otherEtag.getValue()) || etag.getValue().equals(otherEtag.getValue()))
            return Response.notModified(etag);

      }
      else
      {
         // Use strong comparison (ignore weak tags) because HTTP method is not GET
         // or HEAD. If one of tag is weak then tags are not identical.
         if (!etag.isWeak() && !otherEtag.isWeak()
            && ("*".equals(otherEtag.getValue()) || etag.getValue().equals(otherEtag.getValue())))
            return Response.status(Response.Status.PRECONDITION_FAILED);

      }

      // if tags are matched then do as tag 'if-none-match' is absent
      return null;

   }

   /**
    * Comparison for lastModified and unmodifiedSince times.
    * 
    * @param lastModified the last modified time
    * @return ResponseBuilder with status 412 (precondition failed) if
    *         lastModified time is greater then unmodifiedSince otherwise return
    *         null. If date format in header If-Unmodified-Since is wrong also
    *         null returned
    */
   private ResponseBuilder evaluateIfModified(long lastModified)
   {
      String ifUnmodified = getRequestHeaders().getFirst(IF_UNMODIFIED_SINCE);

      if (ifUnmodified == null)
         return null;
      try
      {
         long unmodifiedSince = HeaderHelper.parseDateHeader(ifUnmodified).getTime();
         if (lastModified > unmodifiedSince)
            return Response.status(Response.Status.PRECONDITION_FAILED);

      }
      catch (IllegalArgumentException e)
      {
         if (LOG.isTraceEnabled())
         {
            LOG.trace("An exception occurred: " + e.getMessage());
         }
      }

      return null;
   }

   /**
    * Comparison for lastModified and modifiedSince times.
    * 
    * @param lastModified the last modified time
    * @return ResponseBuilder with status 304 (not modified) if lastModified time
    *         is greater then modifiedSince otherwise return null. If date format
    *         in header If-Modified-Since is wrong also null returned
    */
   private ResponseBuilder evaluateIfUnmodified(long lastModified)
   {
      String ifModified = getRequestHeaders().getFirst(IF_MODIFIED_SINCE);

      if (ifModified == null)
      {
         return null;
      }
      try
      {
         long modifiedSince = HeaderHelper.parseDateHeader(ifModified).getTime();
         if (lastModified < modifiedSince)
         {
            return Response.notModified();
         }
      }
      catch (IllegalArgumentException e)
      {
         if (LOG.isTraceEnabled())
         {
            LOG.trace("An exception occurred: " + e.getMessage());
         }
      }

      return null;
   }
  
   @Override
   public ResponseBuilder evaluatePreconditions() {
      List<String> ifMatch = getRequestHeaders().get(HttpHeaders.IF_MATCH);
      if (ifMatch != null) {
          for (String value : ifMatch) {
              if (!"*".equals(value)) {
                  return Response.status(Response.Status.PRECONDITION_FAILED).tag(EntityTag.valueOf(value));
              }
          }
      }
      return null;
   }

}
