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

import org.exoplatform.commons.utils.PropertyManager;
import org.exoplatform.container.RootContainer.PortalContainerInitTask;
import org.exoplatform.container.RootContainer.PortalContainerPostInitTask;
import org.exoplatform.container.RootContainer.PortalContainerPreInitTask;
import org.exoplatform.container.definition.PortalContainerConfig;
import org.exoplatform.container.util.ContainerUtil;
import org.exoplatform.container.xml.Configuration;
import org.exoplatform.container.xml.PortalContainerInfo;
import org.exoplatform.management.annotations.Managed;
import org.exoplatform.management.annotations.ManagedDescription;
import org.exoplatform.management.jmx.annotations.NameTemplate;
import org.exoplatform.management.jmx.annotations.NamingContext;
import org.exoplatform.management.jmx.annotations.Property;
import org.exoplatform.management.rest.annotations.RESTEndpoint;

import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import jakarta.servlet.ServletContext;

/**
 * Created by The eXo Platform SAS<br>
 *
 * The Portal Container is an object responsible for loading services/components.
 *
 */
@Managed
@NamingContext(@Property(key = "portal", value = "{Name}"))
@NameTemplate({@Property(key = "container", value = "portal"), @Property(key = "name", value = "{Name}")})
@RESTEndpoint(path = "pcontainer")
public class PortalContainer extends ExoContainer
{

   /**
    * Serial Version UID
    */
   private static final long serialVersionUID = -9110532469581690803L;

   /**
    * The default name of the portal container
    */
   public static String DEFAULT_PORTAL_CONTAINER_NAME;

   /**
    * The default name of a the {@link ServletContext} of the rest web application
    */
   public static String DEFAULT_REST_CONTEXT_NAME;

   /**
    * The default name of a the realm
    */
   public static String DEFAULT_REALM_NAME;

   /**
    * The configuration of the portal containers
    */
   private static PortalContainerConfig CONFIG;
   static
   {
      reloadConfig();
   }

   private volatile boolean started_;

   private volatile boolean initialized_;

   private PortalContainerInfo pinfo_;

   /**
    * The name of the portal container
    */
   private final String name;

   /**
    * The comparator used to sort the web applications by priorities 
    */
   private final Comparator<WebAppInitContext> webAppComparator;

   /**
    * The full {@link ServletContext} of the portal container after merging all the 
    * {@link ServletContext} that have been registered
    */
   private final ServletContext portalMergedContext;

   /**
    * The full {@link ClassLoader} of the portal container after merging all the {@link ClassLoader}
    * of the {@link ServletContext} that have been registered
    */
   private final ClassLoader portalMergedClassLoader;

   /**
    * The {@link Set} of all the web application context that share configuration
    */
   private volatile Set<WebAppInitContext> webAppContexts;

   /**
    * To allow overriding we need to have a custom {@link ClassLoader} by web applications by portal
    * containers
    */
   private volatile Map<String, ClassLoader> webAppClassLoaders;

   /**
    * The {@link ServletContext} of the current portal container
    */
   final ServletContext portalContext;

   public PortalContainer(RootContainer parent, final ServletContext portalContext)
   {
      super(parent);
      this.name = ContainerUtil.getServletContextName(portalContext);
      context.setName(name);

      pinfo_ = new PortalContainerInfo(portalContext);
      registerComponentInstance(ServletContext.class, portalContext);
      registerComponentInstance(PortalContainerInfo.class, pinfo_);
      final PortalContainerConfig config = parent.getPortalContainerConfig();
      final List<String> dependencies = config == null ? null : config.getDependencies(name);
      if (dependencies == null || dependencies.isEmpty())
      {
         // No order is required
         this.webAppComparator = null;
      }
      else
      {
         this.webAppComparator = new WebAppInitContextComparator(dependencies);
      }
      this.webAppContexts = Collections.singleton(new WebAppInitContext(portalContext));
      this.portalContext = portalContext;
      this.portalMergedContext = new PortalContainerContext(this);
      this.portalMergedClassLoader = new PortalContainerClassLoader(PortalContainer.this);
      this.webAppClassLoaders = Collections.unmodifiableMap(Collections.singletonMap(name, portalMergedClassLoader));
   }

   /**
    * @return a {@link Set} ordered by priority of all the {@link WebAppInitContext} that represents 
    * the full portal context
    */
   Set<WebAppInitContext> getWebAppInitContexts()
   {
      return webAppContexts;
   }

   /**
    * This gives the merged {@link ClassLoader} between the {@link PortalContainerClassLoader} and the
    * {@link ClassLoader} of the web application.
    * 
    * @param context the {@link ServletContext} of the web application
    * @return the merged {@link ClassLoader} between the {@link PortalContainerClassLoader} and
    * the {@link ClassLoader} of the web application that allows us to override resources contained
    * into the {@link ClassLoader} of the web application 
    */
   public ClassLoader getWebAppClassLoader(ServletContext context)
   {
      final String contextName = ContainerUtil.getServletContextName(context);
      ClassLoader cl = webAppClassLoaders.get(contextName);
      if (cl == null)
      {
         synchronized (this)
         {
            cl = webAppClassLoaders.get(contextName);
            if (cl == null)
            {
               cl =
                  UnifiedClassLoader.createUnifiedClassLoaderInPrivilegedMode(new ClassLoader[]{
                     Thread.currentThread().getContextClassLoader(), portalMergedClassLoader});
               Map<String, ClassLoader> cls = new HashMap<String, ClassLoader>(webAppClassLoaders);
               cls.put(contextName, cl);
               this.webAppClassLoaders = Collections.unmodifiableMap(cls);
            }
         }
      }
      return cl;
   }

   /**
    * @return the full {@link ClassLoader} of the portal container after merging all the 
    * {@link ClassLoader} of all {@link ServletContext} that have been registered
    */
   public ClassLoader getPortalClassLoader()
   {
      return portalMergedClassLoader;
   }

   /**
    * @return the full {@link ServletContext} of the portal container after merging all the 
    * {@link ServletContext} that have been registered
    */
   public ServletContext getPortalContext()
   {
      return portalMergedContext;
   }

   /**
    * Register a new servlet context that contains configuration files and potentially resource files
    * We assume that this method is called within the initialization context of the related web application
    * @param context the {@link ServletContext} of the web application to register
    */
   public synchronized void registerContext(ServletContext context)
   {
      final WebAppInitContext webappCtx = new WebAppInitContext(context);
      if (!webAppContexts.contains(webappCtx))
      {
         final Set<WebAppInitContext> contexts;
         if (webAppComparator == null)
         {
            contexts = new HashSet<WebAppInitContext>(webAppContexts);
         }
         else
         {
            contexts = new TreeSet<WebAppInitContext>(webAppComparator);
            contexts.addAll(webAppContexts);
         }
         contexts.add(webappCtx);
         this.webAppContexts = Collections.unmodifiableSet(contexts);
      }
   }

   /**
    * Unregister a servlet context that contains configuration files and potentially resource files
    * @param context the {@link ServletContext} of the web application to unregister
    */
   public synchronized void unregisterContext(ServletContext context)
   {
      final WebAppInitContext webappCtx = new WebAppInitContext(context);
      if (webAppContexts.contains(webappCtx))
      {
         final Set<WebAppInitContext> contexts;
         if (webAppComparator == null)
         {
            contexts = new HashSet<WebAppInitContext>(webAppContexts);
         }
         else
         {
            contexts = new TreeSet<WebAppInitContext>(webAppComparator);
            contexts.addAll(webAppContexts);
         }
         contexts.remove(webappCtx);
         this.webAppContexts = Collections.unmodifiableSet(contexts);
      }
   }
   /**
    * @return returns the portal container name
    */
   @Managed
   @ManagedDescription("The portal container name")
   public String getName()
   {
      return name;
   }
   /**
    * @return returns the configuration of the container in XML format
    */
   @Managed
   @ManagedDescription("The configuration of the container in XML format.")
   public String getConfigurationXML()
   {
      Configuration conf = getConfiguration();
      if (conf == null)
      {
         LOG.warn("The configuration of the PortalContainer could not be found");
         return null;
      }
      Configuration result = Configuration.merge(((ExoContainer)parent).getConfiguration(), conf);
      if (result == null)
      {
         LOG.warn("The configurations could not be merged");
         return null;
      }
      return result.toXML();
   }

   public PortalContainerInfo getPortalContainerInfo()
   {
      return pinfo_;
   }

   /**
    * @return the current instance of {@link PortalContainer} that has been stored into the related
    * {@link ThreadLocal}. If no value has been set the default portal container will be returned
    */
   public static PortalContainer getInstance()
   {
      PortalContainer container = getInstanceIfPresent();
      if (container == null)
      {
         container = RootContainer.getInstance().getPortalContainer(DEFAULT_PORTAL_CONTAINER_NAME);
         final PortalContainer currentPortalContainer = container;
         PortalContainer.setInstance(currentPortalContainer);
      }
      return container;
   }

   /**
    * @return the current instance of {@link ExoContainer} that has been stored into the
    * {@link ThreadLocal} of {@link ExoContainerContext}. If no {@link PortalContainer} has been set, 
    * it will return <code>null</code>
    */
   public static PortalContainer getInstanceIfPresent()
   {
      ExoContainer container = ExoContainerContext.getCurrentContainerIfPresent();
      if (container instanceof PortalContainer)
      {
         return (PortalContainer)container;
      }
      return null;
   }

   /**
    * @see PortalContainerConfig#isPortalContainerName(String)
    * @param name the portal container name
    */
   public static boolean isPortalContainerName(String name)
   {
      if (CONFIG == null)
      {
         return DEFAULT_PORTAL_CONTAINER_NAME.equals(name);
      }
      else
      {
         return CONFIG.isPortalContainerName(name);
      }
   }

   /**
    * @see PortalContainerConfig#isPortalContainerNameDisabled(String)
    * @param name the portal container name
    */
   public static boolean isPortalContainerNameDisabled(String name)
   {
      if (CONFIG == null)
      {
         return false;
      }
      else
      {
         return CONFIG.isPortalContainerNameDisabled(name);
      }
   }

   /**
    * Add an init-task to all the portal container instances related to the given ServletContext
    * 
    * @param context the context from which we extract the context name
    * @param task the task to execute
    */
   public static void addInitTask(ServletContext context, PortalContainerInitTask task)
   {
      addInitTask(context, task, null);
   }

   /**
    * Add an init-task to all the portal container instances related to the given ServletContext if the
    * given portal container name is <code>null</code> other it will execute the task only of this
    * portal container if the {@link ServletContext} is on of its dependencies
    * 
    * @param context the context from which we extract the context name
    * @param task the task to execute
    * @param portalContainerName the name of the portal container for which we want to execute the task
    */
   public static void addInitTask(ServletContext context, PortalContainerInitTask task, String portalContainerName)
   {
      if (context == null || CONFIG == null)
      {
         return;
      }
      String contextName = ContainerUtil.getServletContextName(context);
      List<String> portalContainerNames = CONFIG.getPortalContainerNames(contextName);
      RootContainer root = RootContainer.getInstance();
      // We assume that we have at least one portal container otherwise there is a bug in PortalContainerConfig
      for (String name : portalContainerNames)
      {
         if (portalContainerName == null || portalContainerName.equals(name))
         {
            root.addInitTask(context, task, name);
         }
      }
   }

   /**
    * Gives the first portal container instance related to the given ServletContext
    * 
    * @param context the context from which we extract the context name
    */
   public static PortalContainer getInstance(ServletContext context)
   {
      if (context == null || CONFIG == null)
      {
         return null;
      }
      String portalContainerName = CONFIG.getPortalContainerName(ContainerUtil.getServletContextName(context));
      if (portalContainerName == null)
      {
         if (PropertyManager.isDevelopping())
         {
            LOG.warn("The Servlet Context '" + ContainerUtil.getServletContextName(context) + "' has not been registered"
               + " has a dependency of any PortalContainerDefinitions.");
         }
         return null;
      }
      RootContainer root = RootContainer.getInstance();
      return root.getPortalContainer(portalContainerName);
   }

   /**
    * We first try to get the ExoContainer that has been stored into the ThreadLocal
    * if the value is of type PortalContainer, we return it otherwise we get the
    * portal container corresponding the given servlet context
    * 
    * @param context the context from which we extract the portal container name
    */
   public static PortalContainer getCurrentInstance(ServletContext context)
   {
      final PortalContainer container = getInstanceIfPresent();
      if (container == null)
      {
         return PortalContainer.getInstance(context);
      }
      return container;
   }

   /**
    * Returns the name of the current portal container that has been stored into the ThreadLocal. If no
    * value can be found the value of PortalContainer.DEFAULT_PORTAL_CONTAINER_NAME will be used
    */
   public static String getCurrentPortalContainerName()
   {
      final PortalContainer container = getInstanceIfPresent();
      if (container == null)
      {
         return DEFAULT_PORTAL_CONTAINER_NAME;
      }
      else
      {
         return container.getName();
      }
   }

   /**
    * Returns the name of the current rest context corresponding to the portal container
    * that has been stored into the ThreadLocal. If no value can be found the value of 
    * PortalContainer.DEFAULT_REST_CONTEXT_NAME will be used
    */
   public static String getCurrentRestContextName()
   {
      final String containerName = getCurrentPortalContainerName();
      return getRestContextName(containerName);
   }

   /**
    * Returns the name of the rest context corresponding to the given portal container name
    * @param portalContainerName the name of the portal container for which we want the
    * name of the rest {@link ServletContext}
    */
   public static String getRestContextName(String portalContainerName)
   {
      if (CONFIG == null)
      {
         return DEFAULT_REST_CONTEXT_NAME;
      }
      return CONFIG.getRestContextName(portalContainerName);
   }

   /**
    * Returns the name of the rest context corresponding to the current portal container
    * @return returns the name of the rest context
    */
   public String getRestContextName()
   {
      return getRestContextName(getName());
   }

   /**
    * Returns the name of the current realm corresponding to the portal container
    * that has been stored into the ThreadLocal. If no value can be found the value of 
    * PortalContainer.DEFAULT_REALM_NAME will be used
    * @return returns the name of the current realm corresponding to the portal container
    */
   public static String getCurrentRealmName()
   {
      final String containerName = getCurrentPortalContainerName();
      return getRealmName(containerName);
   }

   /**
    * Returns the name of the realm corresponding to the given portal container name
    * @param portalContainerName the name of the portal container for which we want the
    * name of the realm
    * @return returns the name of the realm corresponding to the given portal container name
    */
   public static String getRealmName(String portalContainerName)
   {
      if (CONFIG == null)
      {
         return DEFAULT_REALM_NAME;
      }
      return CONFIG.getRealmName(portalContainerName);
   }

   /**
    * Returns the name of the realm corresponding to the current portal container
    * @return returns the name of the realm corresponding to the current portal container
    */
   public String getRealmName()
   {
      return getRealmName(getName());
   }

   /**
    * Returns the current value of the setting corresponding to the portal container
    * that has been stored into the ThreadLocal. If no value can be found, <code>null</code> will be
    * returned
    * @param settingName the name of the setting wanted
    * @return returns the current value of the setting corresponding to the portal container
    */
   public static Object getCurrentSetting(String settingName)
   {
      final String containerName = getCurrentPortalContainerName();
      return getSetting(containerName, settingName);
   }

   /**
    * Returns the value of the setting corresponding to the given portal container name
    * and the given setting name
    * @param portalContainerName the name of the portal container for which we want the
    * name of the value of the setting
    * @param settingName the name of the setting wanted
    * @return returns the value of the setting corresponding to the given portal container name
    */
   public static Object getSetting(String portalContainerName, String settingName)
   {
      if (CONFIG == null)
      {
         return null;
      }
      return CONFIG.getSetting(portalContainerName, settingName);
   }

   /**
    * Returns the value of the setting corresponding to the current portal container 
    * @param settingName the name of the setting wanted
    * @return returns the value of the setting
    */
   public Object getSetting(String settingName)
   {
      return getSetting(getName(), settingName);
   }

   /**
    * Indicates if the given servlet context is a dependency of the given portal container
    * @param container the portal container
    * @param context the {@link ServletContext}
    * @return <code>true</code> if the dependencies matches, <code>false</code> otherwise;
    */
   public static boolean isScopeValid(PortalContainer container, ServletContext context)
   {
      if (CONFIG == null)
      {
         return true;
      }
      return CONFIG.isScopeValid(container.getName(), ContainerUtil.getServletContextName(context));
   }

   @Managed
   public boolean isStarted()
   {
      return started_;
   }

   @Override
   public void start()
   {
     if (initialized_) {
       super.start();
     } else {
       // The startup of PortalContainer should invoke
       // the container lifecycle listeners
       initialized_ = true;
       // the start(true) method will call again this method  start()
       // after listeners injections, so the flag initialized_ should be turned on
       super.start(true);
     }
     started_ = true;
   }

   @Override
   public void stop()
   {
      super.stop();
      started_ = false;
   }

   public static void setInstance(PortalContainer instance)
   {
      ExoContainerContext.setCurrentContainer(instance);
   }

   public static Object getComponent(Class<?> key)
   {
      PortalContainer pcontainer = getInstanceIfPresent();
      return pcontainer.getComponentInstanceOfType(key);
   }

   /**
    * This class is used to compare the {@link WebAppInitContext}
    */
   static class WebAppInitContextComparator implements Comparator<WebAppInitContext>
   {

      private final List<String> dependencies;

      WebAppInitContextComparator(List<String> dependencies)
      {
         this.dependencies = dependencies;
      }

      /**
       * This will sort all the {@link WebAppInitContext} such that we will first have
       * all the web applications defined in the list of dependencies of the 
       * related portal container (see {@link PortalContainerConfig} for more details
       *  about the dependencies) ordered in the same order as the dependencies, then
       *  we will have all the web applications undefined ordered by context name
       */
      public int compare(WebAppInitContext ctx1, WebAppInitContext ctx2)
      {
         int idx1 = dependencies.indexOf(ctx1.getServletContextName());
         int idx2 = dependencies.indexOf(ctx2.getServletContextName());
         if (idx1 == -1 && idx2 != -1)
         {
            return 1;
         }
         else if (idx1 != -1 && idx2 == -1)
         {
            return -1;
         }
         else if (idx1 == -1 && idx2 == -1)
         {
            return ctx1.getServletContextName().compareTo(ctx2.getServletContextName());
         }
         else
         {
            return idx1 - idx2;
         }
      }
   }

   /**
    * Reloads the portal container configuration
    */
   static void reloadConfig()
   {
      ExoContainer top = ExoContainerContext.getTopContainer();
      CONFIG = top instanceof RootContainer ? ((RootContainer)top).getPortalContainerConfig() : null;
      if (CONFIG == null)
      {
         DEFAULT_PORTAL_CONTAINER_NAME = PortalContainerConfig.DEFAULT_PORTAL_CONTAINER_NAME;
         DEFAULT_REST_CONTEXT_NAME = PortalContainerConfig.DEFAULT_REST_CONTEXT_NAME;
         DEFAULT_REALM_NAME = PortalContainerConfig.DEFAULT_REALM_NAME;
      }
      else
      {
         DEFAULT_PORTAL_CONTAINER_NAME = CONFIG.getDefaultPortalContainer();
         DEFAULT_REST_CONTEXT_NAME = CONFIG.getDefaultRestContext();
         DEFAULT_REALM_NAME = CONFIG.getDefaultRealmName();
      }
   }
   
   /**
    * This class is used to register a portal container
    */
   public static class RegisterTask extends PortalContainerPreInitTask
   {
      public void execute(ServletContext context, PortalContainer portalContainer)
      {
         portalContainer.registerContext(context);
      }
   }
   
   /**
    * This class is used to unregister a portal container
    */
   public static class UnregisterTask extends PortalContainerPostInitTask
   {
      public void execute(ServletContext context, PortalContainer portalContainer)
      {
         portalContainer.unregisterContext(context);
      }
   }
}
