/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 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.container.util;

import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;

import org.exoplatform.commons.utils.ClassLoading;
import org.exoplatform.commons.utils.PropertiesLoader;
import org.exoplatform.commons.utils.Tools;
import org.exoplatform.container.ExoContainer;
import org.exoplatform.container.configuration.ConfigurationManager;
import org.exoplatform.container.context.DefinitionException;
import org.exoplatform.container.xml.Component;
import org.exoplatform.container.xml.ComponentLifecyclePlugin;
import org.exoplatform.container.xml.ContainerLifecyclePlugin;
import org.exoplatform.container.xml.Deserializer;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.NormalScope;
import javax.enterprise.inject.Stereotype;
import javax.enterprise.inject.UnproxyableResolutionException;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Scope;
import javax.inject.Singleton;
import jakarta.servlet.ServletContext;


@SuppressWarnings("rawtypes")
public class ContainerUtil
{
   /** The logger. */
   private static final Log LOG = ExoLogger.getExoLogger(ContainerUtil.class);

   private static final class MethodFilterHolder
   {
      private static final MethodFilter METHOD_FILTER = new MethodFilter()
      {
         public boolean isHandled(Method method)
         {
            return !method.getDeclaringClass().equals(Object.class);
         }
      };
   }

   private static final Comparator<Constructor<?>> CONSTRUCTOR_COMPARATOR = new Comparator<Constructor<?>>()
   {
      public int compare(Constructor<?> o1, Constructor<?> o2)
      {
         return o2.getParameterTypes().length - o1.getParameterTypes().length;
      }
   };

   public static Constructor<?>[] getSortedConstructors(Class<?> clazz) throws NoClassDefFoundError
   {
      Constructor<?>[] constructors = clazz.getDeclaredConstructors();
      for (int i = 0; i < constructors.length; i++)
      {
         Constructor<?> c = constructors[i];
         if (c.isAnnotationPresent(Inject.class))
         {
            // This constructor has the annotation Inject so we will use it
            return new Constructor<?>[]{c};
         }
      }
      constructors = clazz.getConstructors();
      Arrays.sort(constructors, CONSTRUCTOR_COMPARATOR);
      return constructors;
   }

   /**
    * Indicates whether or not the given Class has a constructor annotated with Inject
    */
   public static boolean hasInjectableConstructor(Class<?> clazz)
   {
      Constructor<?>[] constructors = clazz.getDeclaredConstructors();
      for (int i = 0; i < constructors.length; i++)
      {
         Constructor<?> c = constructors[i];
         if (c.isAnnotationPresent(Inject.class))
         {
            // There is a constructor annotated with Inject
            return true;
         }
      }
      return false;
   }

   /**
    * Indicates whether or not the given Class has only a public non argument constructor
    */
   public static boolean hasOnlyEmptyPublicConstructor(Class<?> clazz)
   {
      Constructor<?>[] constructors = clazz.getConstructors();
      // No constructor annotated with Inject but we have only a no-argument public constructor
      // In that case it is optional according to the JSR 330
      return (constructors.length == 1 && constructors[0].getParameterTypes().length == 0 && Modifier
         .isPublic(constructors[0].getModifiers()));
   }

   /**
    * Indicates whether or not this class or one of its super class has at least one 
    * Inject annotation on a field or a method
    */
   public static boolean hasFieldOrMethodWithInject(Class<?> clazz)
   {
      if (clazz == null || clazz.equals(Object.class))
      {
         return false;
      }
      Field[] fields = clazz.getDeclaredFields();
      for (int i = 0; i < fields.length; i++)
      {
         if (fields[i].isAnnotationPresent(Inject.class))
         {
            return true;
         }
      }
      Method[] methods = clazz.getDeclaredMethods();
      for (int i = 0; i < methods.length; i++)
      {
         if (methods[i].isAnnotationPresent(Inject.class))
         {
            return true;
         }
      }
      return hasFieldOrMethodWithInject(clazz.getSuperclass());
   }

   /**
    * Gives the scope defined for the given class
    * @param clazz the class for which we want the scope
    * @return a class representing the annotation type of the scope
    * @throws DefinitionException in case the definition of the scope is not correct
    */
   public static Class<? extends Annotation> getScope(Class<?> clazz) throws DefinitionException
   {
      return getScope(clazz, false);
   }

   /**
    * Gives the scope defined for the given class
    * @param clazz the class for which we want the scope
    * @param ignoreExplicit indicates whether the explicit scope must be ignored
    * @return a class representing the annotation type of the scope
    * @throws DefinitionException in case the definition of the scope is not correct
    */
   public static Class<? extends Annotation> getScope(Class<?> clazz, boolean ignoreExplicit)
      throws DefinitionException
   {
      Annotation[] annotations = clazz.getAnnotations();
      Class<? extends Annotation> scope = null;
      Class<? extends Annotation> defaultScope = null;
      boolean hasStereotype = false;
      for (int i = 0; i < annotations.length; i++)
      {
         Annotation annotation = annotations[i];
         Class<? extends Annotation> annotationType = annotation.annotationType();
         if (!ignoreExplicit
            && (annotationType.isAnnotationPresent(Scope.class) || annotationType
               .isAnnotationPresent(NormalScope.class)))
         {
            if (scope != null)
            {
               throw new DefinitionException("You cannot set several scopes to the class " + clazz.getName());
            }
            scope = annotationType;
         }
         else if (annotationType.isAnnotationPresent(Stereotype.class))
         {
            hasStereotype = true;
            Annotation[] stereotypeAnnotations = annotationType.getAnnotations();
            for (int j = 0; j < stereotypeAnnotations.length; j++)
            {
               Annotation stereotypeAnnotation = stereotypeAnnotations[j];
               Class<? extends Annotation> stereotypeAnnotationType = stereotypeAnnotation.annotationType();
               if (stereotypeAnnotationType.isAnnotationPresent(Scope.class)
                  || stereotypeAnnotationType.isAnnotationPresent(NormalScope.class))
               {
                  if (defaultScope != null && !defaultScope.equals(stereotypeAnnotationType))
                  {
                     throw new DefinitionException("The class " + clazz.getName()
                        + " has stereotypes with different default scope");
                  }
                  defaultScope = stereotypeAnnotationType;
               }
            }
         }
      }
      if (scope != null)
         return scope;
      if (defaultScope != null)
         return defaultScope;
      if (hasStereotype)
      {
         throw new DefinitionException("The class " + clazz.getName()
            + " has at least one stereotype but doesn't have any scope, please set an explicit scope");
      }
      return null;
   }

   /**
    * Indicates whether or not the given Class is a singleton or as the scope set to ApplicationScoped
    */
   public static boolean isSingleton(Class<?> clazz)
   {
      Class<? extends Annotation> scope = getScope(clazz);
      if (scope != null)
      {
         return scope.equals(Singleton.class) || scope.equals(ApplicationScoped.class);
      }
      boolean hasInjectableConstructor = hasInjectableConstructor(clazz);
      boolean hasOnlyEmptyPublicConstructor = hasOnlyEmptyPublicConstructor(clazz);
      if (!hasInjectableConstructor && !hasOnlyEmptyPublicConstructor)
      {
         // There is no constructor JSR 330 compliant so it is the old mode
         return true;
      }
      else if (hasInjectableConstructor)
      {
         // There is at least one constructor annotated with Inject so we know that we expect the new mode
         return clazz.isAnnotationPresent(Singleton.class);
      }
      // We have only one public non argument constructor which is compliant with both modes
      if (hasFieldOrMethodWithInject(clazz))
      {
         // There is at least one field or a method annotated with Inject so we expect the new mode
         return clazz.isAnnotationPresent(Singleton.class);
      }
      return true;
   }

   public static Collection<URL> getConfigurationURL(final String configuration) throws Exception
   {
      final ClassLoader cl = Thread.currentThread().getContextClassLoader();

      Collection c = Collections.list(cl.getResources(configuration));

      Map<String, URL> map = new LinkedHashMap<>();
      Iterator i = c.iterator();
      String forbiddenSuffix = "WEB-INF/" + configuration;
      while (i.hasNext())
      {
         URL url = (URL)i.next();
         String key = url.toString();
         // The content of the WEB-INF folder is part of the CL in JBoss AS 6 so 
         // we have to get rid of it in order to prevent any boot issues
         if (key.endsWith(forbiddenSuffix))
         {
            continue;
         }
         map.put(key, url);
      }

      return map.values();
   }

   public static void addContainerLifecyclePlugin(ExoContainer container, ConfigurationManager conf)
   {
      Iterator i = conf.getConfiguration().getContainerLifecyclePluginIterator();
      while (i.hasNext())
      {
         ContainerLifecyclePlugin plugin = (ContainerLifecyclePlugin)i.next();
         addContainerLifecyclePlugin(container, plugin);
      }
   }

   private static void addContainerLifecyclePlugin(ExoContainer container, ContainerLifecyclePlugin plugin)
   {
      try
      {
         Class<?> clazz = ClassLoading.forName(plugin.getType(), ContainerUtil.class);
         org.exoplatform.container.ContainerLifecyclePlugin cplugin =
            (org.exoplatform.container.ContainerLifecyclePlugin)container
               .createComponent(clazz, plugin.getInitParams());
         cplugin.setName(plugin.getName());
         cplugin.setDescription(plugin.getDescription());
         container.addContainerLifecylePlugin(cplugin);
      }
      catch (Exception ex)
      {
         LOG.error("Failed to instanciate plugin " + plugin.getType() + ": " + ex.getMessage(), ex);
      }
   }

   public static void addComponentLifecyclePlugin(ExoContainer container, ConfigurationManager conf)
   {
      Collection plugins = conf.getConfiguration().getComponentLifecyclePlugins();
      Iterator i = plugins.iterator();
      while (i.hasNext())
      {
         ComponentLifecyclePlugin plugin = (ComponentLifecyclePlugin)i.next();
         try
         {
            Class<?> classType = ClassLoading.loadClass(plugin.getType(), ContainerUtil.class);
            org.exoplatform.container.component.ComponentLifecyclePlugin instance =
               (org.exoplatform.container.component.ComponentLifecyclePlugin)classType.newInstance();
            container.addComponentLifecylePlugin(instance);
         }
         catch (Exception ex)
         {
            LOG.error("Failed to instanciate plugin " + plugin.getType() + ": " + ex.getMessage(), ex);
         }
      }
   }

   public static void addComponents(ExoContainer container, ConfigurationManager conf)
   {
      Collection components = conf.getComponents();
      if (components == null)
         return;
      Iterator i = components.iterator();
      while (i.hasNext())
      {
         Component component = (Component)i.next();
         String type = component.getType();
         String key = component.getKey();
         try
         {
            Class<?> classType = ClassLoading.loadClass(type, ContainerUtil.class);
            if (key == null)
            {
               if (component.isMultiInstance())
               {
                  throw new UnsupportedOperationException("Multi-instance isn't allowed anymore");
               }
               else
               {
                  container.registerComponentImplementation(classType);
               }
            }
            else
            {
               try
               {
                  Class<?> keyType = ClassLoading.loadClass(key, ContainerUtil.class);
                  if (component.isMultiInstance())
                  {
                     throw new UnsupportedOperationException("Multi-instance isn't allowed anymore");
                  }
                  else
                  {
                     container.registerComponentImplementation(keyType, classType);
                  }
               }
               catch (Exception ex)
               {
                  container.registerComponentImplementation(key, classType);
               }
            }
         }
         catch (ClassNotFoundException ex)
         {
            LOG.error("Cannot register the component corresponding to key = '" + key + "' and type = '" + type + "'",
               ex);
         }
      }
   }

   /**
    * Loads the properties file corresponding to the given url
    * @param url the url of the properties file
    * @return a {@link Map} of properties
    */
   public static Map<String, String> loadProperties(URL url)
   {
      return loadProperties(url, true);
   }

   /**
    * Loads the properties file corresponding to the given url
    * @param url the url of the properties file
    * @param resolveVariables indicates if the variables must be resolved
    * @return a {@link Map} of properties
    */
   public static Map<String, String> loadProperties(URL url, boolean resolveVariables)
   {
      LinkedHashMap<String, String> props = null;
      String path = null;
      InputStream in = null;
      try
      {
         //
         if (url != null)
         {
            path = url.getPath();
            in = url.openStream();
         }

         //
         if (in != null)
         {
            String fileName = url.getFile();
            if (Tools.endsWithIgnoreCase(path, ".properties"))
            {
               if (LOG.isDebugEnabled())
                  LOG.debug("Attempt to load property file " + path);
               props = PropertiesLoader.load(in);
            }
            else if (Tools.endsWithIgnoreCase(fileName, ".xml"))
            {
               if (LOG.isDebugEnabled())
                  LOG.debug("Attempt to load property file " + path + " with XML format");
               props = PropertiesLoader.loadFromXML(in);
            }
            else if (LOG.isDebugEnabled())
            {
               LOG.debug("Will not load property file" + path + " because its format is not recognized");
            }
            if (props != null && resolveVariables)
            {
               // Those properties are used for variables resolution
               final Map<String, Object> currentProps = new HashMap<String, Object>();
               for (Map.Entry<String, String> entry : props.entrySet())
               {
                  String propertyName = entry.getKey();
                  String propertyValue = entry.getValue();
                  propertyValue = Deserializer.resolveVariables(propertyValue, currentProps);
                  props.put(propertyName, propertyValue);
                  currentProps.put(propertyName, propertyValue);
               }
            }
         }
         else
         {
            LOG.error("Could not load property file " + path);
         }
      }
      catch (Exception e)
      {
         LOG.error("Cannot load property file " + path, e);
      }
      finally
      {
         if (in != null)
         {
            try
            {
               in.close();
            }
            catch (IOException ignore)
            {
               if (LOG.isTraceEnabled())
               {
                  LOG.trace("An exception occurred: " + ignore.getMessage());
               }
            }
         }
      }
      return props;
   }

   /**
    * Creates a proxy of the given super class whose instance will be created accessed lazily thanks to a provider
    * @param superClass the super class of the proxy to create
    * @param provider the provider that will create the instance lazily
    * @return a proxy of the given super class
    * @throws UnproxyableResolutionException if any issue occurs while creating the proxy
    */
   public static <T> T createProxy(final Class<T> superClass, final Provider<T> provider)
      throws UnproxyableResolutionException
   {
      Method[] methods = superClass.getDeclaredMethods();
      for (int i = 0; i < methods.length; i++)
      {
         Method m = methods[i];
         int modifiers = m.getModifiers();
         if (Modifier.isFinal(modifiers) && !Modifier.isPrivate(modifiers) && !Modifier.isStatic(modifiers))
         {
            throw new UnproxyableResolutionException(
                "Cannot create a proxy for the class "
                    + superClass.getName()
                    + " because it has at least one non-static, final method with public, protected or default visibility");
         }
      }
      try
      {
         ProxyFactory factory = new ProxyFactory();
         factory.setSuperclass(superClass);
         factory.setFilter(MethodFilterHolder.METHOD_FILTER);
         MethodHandler handler = new ProxyMethodHandler<T>(provider);
         return superClass.cast(factory.create(new Class<?>[0], new Object[0], handler));
      }
      catch (Exception e)
      {
         throw new UnproxyableResolutionException("Cannot create a proxy for the class " + superClass.getName(),
                                                  e);
      }
   }

   /**
    * Gives the context name thanks to the method {@link ServletContext#getServletContextName()} if not <code>null</code>
    * otherwise it will extract the value from the value of {@link ServletContext#getContextPath()}
    * @param context the context from which we want to extract the name of the context
    * @return the context name
    */
   public static String getServletContextName(ServletContext context)
   {
      String result = context.getServletContextName();
      if (result == null)
      {
         result = context.getContextPath();
         if (result == null)
            return result;
         if (result.startsWith("/"))
            result = result.substring(1);
         int index = result.indexOf('/');
         if (index >= 0)
            result = result.substring(0, index);
      }
      return result;
   }

   /**
    * The method handler used for the dynamic Proxies 
    */
   private static class ProxyMethodHandler<T> implements MethodHandler
   {
      private final Provider<T> provider;
      
      public ProxyMethodHandler(Provider<T> provider)
      {
         this.provider = provider;
      }

      /**
       * {@inheritDoc}
       */
      public Object invoke(Object self, Method m, Method proceed, Object[] args) throws Throwable
      {
         if (!m.isAccessible())
            m.setAccessible(true);
         return m.invoke(provider.get(), args);
      }
      
   }
}
