/**
 * 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.container.configuration.ConfigurationException;
import org.exoplatform.container.configuration.ConfigurationManager;
import org.exoplatform.container.configuration.ConfigurationManagerImpl;
import org.exoplatform.container.monitor.jvm.J2EEServerInfo;
import org.exoplatform.container.util.ContainerUtil;
import org.exoplatform.container.xml.Configuration;
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.Property;
import org.exoplatform.management.rest.annotations.RESTEndpoint;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Created by The eXo Platform SAS<br>
 * Singleton, context independent Exo Container with one configuration
 * entry point. The configuration is set as follows: - client calls
 * setConfigurationURL() or setConfigurationPath method BEFORE
 * getInstance() - otherwise container in instantiation time looks for
 * configuration.xml file in the "home" directory. the home directory
 * it is AS server home in a case of AS env or just current directory
 * (from where JVM is started) for standalone.
 * 
 */
@Managed
@NameTemplate(@Property(key = "container", value = "standalone"))
@RESTEndpoint(path = "scontainer")
public class StandaloneContainer extends ExoContainer implements TopExoContainer
{

   /**
    * The logger
    */
   private static final Log LOG = ExoLogger.getLogger("exo.kernel.container.StandaloneContainer");
   
   private static final long serialVersionUID = 12L;

   private static volatile StandaloneContainer container;

   private static volatile URL configurationURL = null;

   private static boolean useDefault = true;

   /**
    * The listeners
    */
   private final List<TopExoContainerListener> listeners = new CopyOnWriteArrayList<TopExoContainerListener>();
   private ConfigurationManagerImpl configurationManager;

   /**
    * Private StandaloneContainer constructor.
    *
    * @param configClassLoader ClassLoader 
    */
   private StandaloneContainer(ClassLoader configClassLoader)
   {
      //
      configurationManager = new ConfigurationManagerImpl(configClassLoader, ExoContainer.getProfilesFromProperty());
      registerComponentInstance(ConfigurationManager.class, configurationManager);
      // Workaround used to allow to use the PropertyConfigurator with the StandaloneContainer
      // If the system property PropertyManager.PROPERTIES_URL has been set properly, it will load the properties
      // from the file and load them as system properties
      new PropertyConfigurator(configurationManager);
   }

   /**
    * Shortcut for getInstance(null, null).
    * 
    * @return the StandaloneContainer instance
    * @throws Exception if error occurs
    */
   public static StandaloneContainer getInstance() throws Exception
   {
      return getInstance(null, null);
   }

   /**
    * Shortcut for getInstance(configClassLoader, null).
    * 
    * @param configClassLoader ClassLoader
    * @return the StandaloneContainer instance
    * @throws Exception if error occurs
    */
   public static StandaloneContainer getInstance(ClassLoader configClassLoader) throws Exception
   {
      return getInstance(configClassLoader, null);
   }

   /**
    * Shortcut for getInstance(null, components).
    * 
    * @param components Object[][]
    * @return the StandaloneContainer instance
    * @throws Exception if error occurs
    */
   public static StandaloneContainer getInstance(Object[][] components) throws Exception
   {
      return getInstance(null, components);
   }

   /**
    * A way to inject externally instantiated objects to container before it
    * starts Object[][] components - an array of components in form: {{"name1",
    * component1}, {"name2", component2}, ...}.
    * 
    * @param configClassLoader ClassLoader 
    * @param components Object[][] 
    * @return the StandaloneContainer instance
    * @throws Exception if error occurs
    */
   public static StandaloneContainer getInstance(ClassLoader configClassLoader, Object[][] components) throws Exception
   {
      if (container == null)
      {
         synchronized (StandaloneContainer.class)
         {
            if (container == null)
            {
               container = createNStartContainer(configClassLoader, components);
            }
         }
      }
      return container;
   }

   /**
    * Creates, initializes and starts the container.
    */
   private static StandaloneContainer createNStartContainer(ClassLoader configClassLoader, Object[][] components) throws Exception,
      MalformedURLException, ConfigurationException
   {
      final StandaloneContainer container = new StandaloneContainer(configClassLoader);
      ExoContainerContext.setTopContainer(container);

      if (useDefault)
         container.initDefaultConf();
      // initialize configurationURL
      initConfigurationURL(configClassLoader);
      container.populate(configurationURL);
      if (components != null)
         container.registerArray(components);
      container.start(true);
      System.setProperty("exo.standalone-container", StandaloneContainer.class.getName());
      LOG.info("StandaloneContainer initialized using:  " + configurationURL);
      container.onStartupComplete();
      return container;
   }

   /**
    * {@inheritDoc}
    */
   protected void initContainerInternal()
   {
      // We have no need to initialize the container since it is already done.
   }

   /**
    * Calls all the listeners to indicate that the startup is complete
    */
   private void onStartupComplete()
   {
      if (!listeners.isEmpty())
      {
         for (TopExoContainerListener listener : listeners)
         {
            listener.onStartupComplete();
         }
      }
   }

   protected void registerArray(Object[][] components)
   {
      for (Object[] comp : components)
      {
         if (comp.length != 2)
            continue;
         if (comp[0] == null || comp[1] == null)
            continue;
         if (!comp[0].getClass().getName().equals(String.class.getName()))
            continue;
         String n = (String)comp[0];
         Object o = comp[1];
         container.registerComponentInstance(n, o);
         LOG.info("StandaloneContainer: injecting \"" + n + "\"");
      }
   }

   /**
    * Add configuration URL. Plugable way of configuration. Add the configuration to existing configurations set.
    *
    * @param url URL of location to configuration file
    * @throws MalformedURLException if path is wrong
    */
   public static void addConfigurationURL(String url) throws MalformedURLException
   {
      if ((url == null) || (url.length() == 0))
         return;
      URL confURL = new URL(url);
      configurationURL = fileExists(confURL) ? confURL : null;
   }

   /**
    * Set configuration URL. The configuration should contains all required components configured.
    *
    * @param url URL of location to configuration file
    * @throws MalformedURLException if path is wrong
    */
   public static void setConfigurationURL(String url) throws MalformedURLException
   {
      useDefault = false;
      addConfigurationURL(url);
   }

   /**
    * Add configuration path. Plugable way of configuration. Add the configuration to existing configurations set.
    *
    * @param path path to configuration file
    * @throws MalformedURLException if path is wrong
    */
   public static void addConfigurationPath(final String path) throws MalformedURLException
   {
      if ((path == null) || (path.length() == 0))
         return;

      URL confURL = new File(path).toURI().toURL();

      configurationURL = fileExists(confURL) ? confURL : null;
   }

   /**
    * Set configuration path. The configuration should contains all required components configured.  
    *
    * @param path path to configuration file
    * @throws MalformedURLException if path is wrong
    */
   public static void setConfigurationPath(String path) throws MalformedURLException
   {
      useDefault = false;
      addConfigurationPath(path);
   }

   /**
    * Get configurationURL.
    *
    * @return URL
    */
   public URL getConfigurationURL()
   {
      return configurationURL;
   }

   @Managed
   @ManagedDescription("The configuration of the container in XML format.")
   public String getConfigurationXML()
   {
      Configuration config = getConfiguration();
      if (config == null)
      {
         LOG.warn("The configuration of the StandaloneContainer could not be found");
         return null;
      }
      return config.toXML();
   }
   
   /**
    * {@inheritDoc}
    */
   @Override
   public synchronized void stop()
   {
      super.stop();
      ExoContainerContext.setTopContainer(null);
      container = null;
   }

   // -------------- Helpers ----------

   private static boolean fileExists(final URL url)
   {
      try
      {
         url.openStream().close();
         return true;
      }
      catch (IOException e)
      {
         return false;
      }
   }

   /**
    * Implements strategy of choosing configuration for this container.
    * 
    * @throws MalformedURLException
    * @throws ConfigurationException
    */
   private static void initConfigurationURL(ClassLoader configClassLoader) throws MalformedURLException,
      ConfigurationException
   {
      // (1) set by setConfigurationURL or setConfigurationPath
      // or
      if (configurationURL == null)
      {
         synchronized (StandaloneContainer.class)
         {
            if (configurationURL == null)
            {
               configurationURL = getConfigurationURL(configClassLoader);
            }
         }
      }
   }

   /**
    * Gives the configuration URL according to the given {@link ClassLoader}
    */
   private static URL getConfigurationURL(ClassLoader configClassLoader) throws MalformedURLException
   {
      final J2EEServerInfo env = new J2EEServerInfo();
      
      // (2) exo-configuration.xml in AS (standalone) home directory
      URL configurationURL = (new File(env.getServerHome() + "/exo-configuration.xml")).toURI().toURL();

      // (3) AS_HOME/conf/exo-conf (JBossAS usecase)
      if (!fileExists(configurationURL))
      {
         configurationURL = (new File(env.getExoConfigurationDirectory() + "/exo-configuration.xml")).toURI().toURL();
      }
      
      // (4) conf/exo-configuration.xml in war/ear(?)
      if (!fileExists(configurationURL) && configClassLoader != null)
      {
         configurationURL = configClassLoader.getResource("conf/exo-configuration.xml");
      }
      return configurationURL;
   }

   private void initDefaultConf() throws Exception
   {
      configurationManager.addConfiguration(ContainerUtil.getConfigurationURL("conf/configuration.xml"));
      configurationManager.addConfiguration(ContainerUtil.getConfigurationURL("conf/portal/configuration.xml"));
      try
      {
         configurationManager.addConfiguration("war:/conf/configuration.xml");
      }
      catch (Exception ex)
      {
         if (LOG.isTraceEnabled())
         {
            LOG.trace("An exception occurred: " + ex.getMessage());
         }
      }
   }

   private void populate(URL conf) throws Exception
   {
      configurationManager.addConfiguration(conf);
      configurationManager.processRemoveConfiguration();
      ContainerUtil.addComponents(StandaloneContainer.this, configurationManager);
   }

   /**
    * {@inheritDoc}
    */
   public void addListener(TopExoContainerListener listener)
   {
      listeners.add(listener);
   }
}
