/*
 * 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.resource;

import org.exoplatform.services.rest.ConstructorDescriptor;
import org.exoplatform.services.rest.FieldInjector;
import org.exoplatform.services.rest.FilterDescriptor;
import org.exoplatform.services.rest.ObjectModel;
import org.exoplatform.services.rest.method.MethodParameter;
import org.exoplatform.services.rest.provider.ProviderDescriptor;
import org.exoplatform.services.rest.resource.AbstractResourceDescriptor;
import org.exoplatform.services.rest.resource.ResourceDescriptor;
import org.exoplatform.services.rest.resource.ResourceDescriptorVisitor;
import org.exoplatform.services.rest.resource.ResourceMethodDescriptor;
import org.exoplatform.services.rest.resource.ResourceMethodMap;
import org.exoplatform.services.rest.resource.SubResourceLocatorDescriptor;
import org.exoplatform.services.rest.resource.SubResourceMethodDescriptor;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import javax.ws.rs.FormParam;
import javax.ws.rs.core.MultivaluedMap;

/**
 * Validate ResourceDescriptors. @see
 * {@link ResourceDescriptor#accept(ResourceDescriptorVisitor)}.
 * <p>
 * Validation Goals:
 * <ul>
 * <li>check number of method parameters without annotation, should be not more
 * then one at resource method or sub-resource method and no one at sub-resource
 * locator</li>
 * <li>if one of parameters at resource method or sub-resource method has
 * {@link FormParam} annotation then entity type can be only
 * MultivalueMap&lt;String, String&gt; and nothing other</li>
 * <li> {@link PathValue#getPath()} can't return empty string, it minds for root
 * resource classes, sub-resource methods and sub-resource locators can't have
 * annotation &#64;Path("")</li>
 * <li>Resource class must contains at least one resource method, sub-resource
 * method or sub-resource locator</li>
 * </ul>
 * <p>
 * Non-Goals:
 * <ul>
 * <li>Check does any two resource methods has the same consume and produce
 * media type. This will be done later in binding cycle</li>
 * <li>Check does any two sub-resource methods has the same consume and produce
 * media type and HTTP request method designation. This will be done later in
 * binding cycle</li>
 * <li>Check does two sub-resource locators has the same UriPattern</li>
 * </ul>
 * <p>
 * 
 * @author <a href="mailto:andrew00x@gmail.com">Andrey Parfonov</a>
 * @version $Id: $
 */
public class ResourceDescriptorValidator implements ResourceDescriptorVisitor
{

   /**
    * Visitor instance.
    */
   private static AtomicReference<ResourceDescriptorValidator> instance =
      new AtomicReference<ResourceDescriptorValidator>();

   /**
    * @return singleton instance of ResourceDescriptorVisitor
    */
   public static ResourceDescriptorValidator getInstance()
   {
      ResourceDescriptorValidator t = instance.get();
      if (t == null)
      {
         instance.compareAndSet(null, new ResourceDescriptorValidator());
         t = instance.get();
      }
      return t;
   }

   /**
    * Validate AbstractResourceDescriptor. AbstractResourceDescriptor is a class
    * which annotated with path annotation then it is root resource, or not
    * annotated with path then it is sub-resource. Can have also consumes and
    * produces annotation. Path annotation is required for root resource.
    * {@inheritDoc}
    */
   public void visitAbstractResourceDescriptor(AbstractResourceDescriptor ard)
   {

      if (ard.isRootResource() && ard.getPathValue().getPath().length() == 0)
      {
         String msg =
            "Resource class " + ard.getObjectClass()
               + " is root resource but path value empty, see javax.ws.rs.Path#value()";
         throw new RuntimeException(msg);
      }

      checkObjectModel(ard);

      int resMethodCount =
         ard.getResourceMethods().size() + ard.getSubResourceMethods().size() + ard.getSubResourceLocators().size();
      if (resMethodCount == 0)
      {
         String msg =
            "Not found any resource methods, sub-resource methods" + " or sub-resource locators in "
               + ard.getObjectClass().getName();
         throw new RuntimeException(msg);
      }

      // check all resource methods
      for (List<ResourceMethodDescriptor> l : ard.getResourceMethods().values())
      {
         for (ResourceMethodDescriptor rmd : l)
            rmd.accept(this);
      }

      // check all sub-resource methods
      for (ResourceMethodMap<SubResourceMethodDescriptor> rmm : ard.getSubResourceMethods().values())
      {
         for (List<SubResourceMethodDescriptor> l : rmm.values())
         {
            for (SubResourceMethodDescriptor rmd : l)
               rmd.accept(this);
         }
      }

      // check all sub-resource locators
      for (SubResourceLocatorDescriptor loc : ard.getSubResourceLocators().values())
      {
         loc.accept(this);
      }

   }

   /**
    * Validate ResourceMethodDescriptor. ResourceMethodDescriptor is method in
    * Resource class which has not path annotation. This method MUST have at
    * least one annotation (HTTP method, e.g. GET). {@inheritDoc}
    */
   public void visitResourceMethodDescriptor(ResourceMethodDescriptor rmd)
   {
      checkMethodParameters(rmd);
   }

   /**
    * Validate SubResourceLocatorDescriptor. SubResourceLocatorDescriptor is a
    * method which annotated with path annotation and has not HTTP method
    * annotation. This method can not directly process the request but it can
    * produces object that will handle the request. {@inheritDoc}
    */
   public void visitSubResourceLocatorDescriptor(SubResourceLocatorDescriptor srld)
   {
      if (srld.getPathValue().getPath().length() == 0)
      {
         String msg =
            "Path value is empty for method " + srld.getMethod().getName() + " in resource class "
               + srld.getParentResource().getObjectClass() + ", see javax.ws.rs.Path#value()";
         throw new RuntimeException(msg);
      }
      checkMethodParameters(srld);
   }

   /**
    * Validate SubResourceMethodDescriptor. SubResourceMethodDescriptor is a
    * method which annotated with path annotation and has HTTP method
    * annotation. This method can process the request directly. {@inheritDoc}
    */
   public void visitSubResourceMethodDescriptor(SubResourceMethodDescriptor srmd)
   {
      if (srmd.getPathValue().getPath().length() == 0)
      {
         String msg =
            "Path value is null or empty for method " + srmd.getMethod().getName() + " in resource class "
               + srmd.getParentResource().getObjectClass() + ", see javax.ws.rs.Path#value()";
         throw new RuntimeException(msg);
      }
      checkMethodParameters(srmd);
   }

   /**
    * Check method parameter for valid annotations. NOTE If a any method
    * parameter is annotated with {@link FormParam} then type of entity
    * parameter must be MultivalueMap&lt;String, String&gt;.
    * 
    * @param rmd See {@link ResourceMethodDescriptor}
    */
   private static void checkMethodParameters(ResourceMethodDescriptor rmd)
   {
      List<MethodParameter> l = rmd.getMethodParameters();
      boolean entity = false;
      boolean form = false;
      for (int i = 0; i < l.size(); i++)
      {
         // Must be only: MatrixParam, QueryParam, PathParam, HeaderParam,
         // FormParam, CookieParam, Context and only one of it at each parameter
         MethodParameter mp = l.get(i);
         if (mp.getAnnotation() == null)
         {
            if (!entity)
            {
               entity = true;
               if (form) // form already met then check type of entity
                  checkFormParam(mp.getParameterClass(), mp.getGenericType());
            }
            else
            {
               String msg =
                  "Wrong or absent annotation at parameter with index " + i + " at "
                     + rmd.getParentResource().getObjectClass() + "#" + rmd.getMethod().getName();
               throw new RuntimeException(msg);
            }

         }
         else
         {
            if (mp.getAnnotation().annotationType() == FormParam.class)
            {
               form = true;
               if (entity) // entity already met then check type of entity
                  checkFormParam(mp.getParameterClass(), mp.getGenericType());
            }
         }
      }
   }

   /**
    * Check does sub-resource locator has required annotation at method
    * parameters. Sub-resource locator can't has not annotated parameter (entity
    * parameter).
    * 
    * @param srld SubResourceLocatorDescriptor
    */
   private static void checkMethodParameters(SubResourceLocatorDescriptor srld)
   {
      List<MethodParameter> l = srld.getMethodParameters();
      for (int i = 0; i < l.size(); i++)
      {
         // Must be only: MatrixParam, QueryParam, PathParam, HeaderParam,
         // FormParam, CookieParam, Context and only one of it at each parameter
         MethodParameter mp = l.get(i);
         if (mp.getAnnotation() == null)
         {
            // not allowed to have not annotated parameters in resource locator
            String msg =
               "Wrong or absent annotation at parameter with index " + i + " at "
                  + srld.getParentResource().getObjectClass() + "#" + srld.getMethod().getName();
            throw new RuntimeException(msg);
         }
      }
   }

   /**
    * Check is supplied class MultivaluedMap&lt;String, String&gt;.
    * 
    * @param clazz class to be checked
    * @param type generic type
    * @see #checkGenericType(Type)
    */
   @SuppressWarnings("rawtypes")
   private static void checkFormParam(Class clazz, Type type)
   {
      if (MultivaluedMap.class != clazz || !checkGenericType(type))
      {
         String msg =
            "If a any method parameter is annotated with FormParam then type"
               + " of entity parameter MUST be MultivalueMap<String, String> or FormEntity";
         throw new RuntimeException(msg);
      }
   }

   /**
    * Check is supplied class parameterized as &lt;String, String&gt;.
    * 
    * @param type generic type
    * @return true if type is {@link ParameterizedType} and parameterized
    *         &lt;String, String&gt;, false otherwise
    */
   private static boolean checkGenericType(Type type)
   {
      if (type instanceof ParameterizedType)
      {
         ParameterizedType pt = (ParameterizedType)type;
         Type[] genericTypes = pt.getActualTypeArguments();
         if (genericTypes.length == 2)
         {
            try
            {
               return (String.class == (Class<?>)genericTypes[0]) && (String.class == (Class<?>)genericTypes[1]);
            }
            catch (ClassCastException e)
            {
               return false;
            }
         }
      }
      // not parameterized type
      return false;
   }

   /**
    * {@inheritDoc}
    */
   public void visitConstructorInjector(ConstructorDescriptor ci)
   {
      // currently nothing to do, should be already valid
   }

   /**
    * {@inheritDoc}
    */
   public void visitFieldInjector(FieldInjector fi)
   {
      // currently nothing to do, should be already valid
   }

   /**
    * {@inheritDoc}
    */
   public void visitFilterDescriptor(FilterDescriptor fd)
   {
      checkObjectModel(fd);
   }

   /**
    * {@inheritDoc}
    */
   public void visitProviderDescriptor(ProviderDescriptor pd)
   {
      checkObjectModel(pd);
   }

   protected void checkObjectModel(ObjectModel model)
   {
      for (ConstructorDescriptor c : model.getConstructorDescriptors())
         c.accept(this);
      for (FieldInjector f : model.getFieldInjectors())
         f.accept(this);
   }

}
