/**
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.exoplatform.services.resources.impl;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.picocontainer.Startable;

import org.exoplatform.commons.cache.future.FutureCache;
import org.exoplatform.commons.cache.future.FutureExoCache;
import org.exoplatform.commons.cache.future.Loader;
import org.exoplatform.commons.utils.IOUtil;
import org.exoplatform.commons.utils.MapResourceBundle;
import org.exoplatform.commons.utils.PageList;
import org.exoplatform.commons.utils.PropertyManager;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.container.RootContainer.PortalContainerPostInitTask;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.services.cache.ExoCache;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.resources.ExoResourceBundle;
import org.exoplatform.services.resources.IdentityResourceBundle;
import org.exoplatform.services.resources.LocaleConfig;
import org.exoplatform.services.resources.LocaleConfigService;
import org.exoplatform.services.resources.PropertiesClassLoader;
import org.exoplatform.services.resources.Query;
import org.exoplatform.services.resources.ResourceBundleData;
import org.exoplatform.services.resources.ResourceBundleService;

import jakarta.servlet.ServletContext;

/**
 * Created by The eXo Platform SAS Mar 9, 2007
 */
public abstract class BaseResourceBundleService implements ResourceBundleService, Startable {

  protected static final Log                                                  LOG         =
                                                                                  ExoLogger.getLogger("org.exoplatform.services.resources");

  protected ClassLoader                                                       cl;

  /**
   * List of local loaded
   */
  protected List<String>                                                      localeList_ =
                                                                                          Collections.synchronizedList(new ArrayList<String>());;

  protected volatile List<String>                                             classpathResources_;

  protected volatile String[]                                                 portalResourceBundleNames_;

  protected LocaleConfigService                                               localeService_;

  protected ExoCache<String, ResourceBundle>                                  cache_;

  protected PortalContainer                                                   portalContainer_;

  private volatile FutureCache<String, ResourceBundle, ResourceBundleContext> futureCache_;

  private final Loader<String, ResourceBundle, ResourceBundleContext>         loader_     =
                                                                                      new Loader<String, ResourceBundle, ResourceBundleContext>() {
                                                                                                                                                                                /**
                                                                                                                                                                                 * {@inheritDoc}
                                                                                                                                                                                 */
                                                                                                                                                                                public ResourceBundle retrieve(ResourceBundleContext context,
                                                                                                                                                                                                               String key) throws Exception {
                                                                                                                                                                                  ResourceBundle resourceBundle =
                                                                                                                                                                                                                context.get(key);
                                                                                                                                                                                  return resourceBundle
                                                                                                                                                                                      == null ?
                                                                                                                                                                                              ExoResourceBundle.NULL_OBJECT :
                                                                                                                                                                                              resourceBundle;
                                                                                                                                                                                }
                                                                                                                                                                              };

  private volatile List<String>                                               initResources_;

  @SuppressWarnings("unchecked")
  protected void initParams(InitParams params) {
    classpathResources_ = params.getValuesParam("classpath.resources").getValues();

    // resources name can use for portlets
    List prnames = params.getValuesParam("portal.resource.names").getValues();
    portalResourceBundleNames_ = new String[prnames.size()];
    for (int i = 0; i < prnames.size(); i++) {
      portalResourceBundleNames_[i] = (String) prnames.get(i);
    }

    initResources_ = params.getValuesParam("init.resources").getValues();
  }

  /**
   * Add new resources bundles
   */
  public synchronized void addResourceBundle(BaseResourceBundlePlugin plugin) {
    List<String> classpathResources = plugin.getClasspathResources();
    if (classpathResources != null && !classpathResources.isEmpty()) {
      List<String> result = new ArrayList<String>(classpathResources);
      if (classpathResources_ != null) {
        result.addAll(classpathResources_);
      }
      this.classpathResources_ = Collections.unmodifiableList(result);
    }
    List<String> portalResources = plugin.getPortalResources();
    if (portalResources != null && !portalResources.isEmpty()) {
      List<String> result = new ArrayList<String>(portalResources);
      if (portalResourceBundleNames_ != null) {
        result.addAll(Arrays.asList(portalResourceBundleNames_));
      }
      this.portalResourceBundleNames_ = (String[]) result.toArray(new String[result.size()]);
    }
    List<String> initResources = plugin.getInitResources();
    if (initResources != null && !initResources.isEmpty()) {
      List<String> result = new ArrayList<String>(initResources);
      if (initResources_ != null) {
        result.addAll(initResources_);
      }
      this.initResources_ = Collections.unmodifiableList(result);
    }
  }

  /**
   * Loads all the "init" resource bundles
   *
   * @see org.picocontainer.Startable#start()
   */
  public void start() {
    PageList pl = null;
    try {
      pl = findResourceDescriptions(new Query(null, null));
    } catch (Exception e) {
      throw new RuntimeException("Cannot check if a resource already exists", e);
    }
    if (pl.getAvailable() > 0)
      return;

    // init resources
    cl = Thread.currentThread().getContextClassLoader();
    final List<String> initResources = initResources_;
    final Collection<LocaleConfig> localeConfigs = localeService_.getLocalConfigs();
    // Load resources for default local
    loadResourcesForLocale(DEFAULT_CROWDIN_LOCALE);

    PortalContainer.addInitTask(portalContainer_.getPortalContext(), new PortalContainerPostInitTask() {
      @Override
      public void execute(ServletContext context, PortalContainer portalContainer) {
        // Execute the load resources in an asynchronous way
        new Thread(new Runnable() {
          @Override
          public void run() {
            // iterate all resources
            for (Iterator<LocaleConfig> iter = localeConfigs.iterator(); iter.hasNext();) {
              LocaleConfig localeConfig = iter.next();
              // Skip loaded resources before container startup
              if (!localeList_.contains(localeConfig.getLocale().toLanguageTag())) {
                loadResourcesForLocale(localeConfig.getLocale());
              }
            }
          }
        }, "ResourceBundleThread").start();
      }
    });
  }

  public ResourceBundle getResourceBundle(String[] name, Locale locale) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return getResourceBundle(name, locale, cl);
  }

  public ResourceBundle getResourceBundle(String name, Locale locale) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return getResourceBundle(name, locale, cl);
  }

  public String[] getSharedResourceBundleNames() {
    return portalResourceBundleNames_;
  }

  @Override
  public String getSharedString(String key, Locale locale) {
    ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
    Thread.currentThread().setContextClassLoader(portalContainer_.getPortalClassLoader());
    try {
      String[] sharedResourceBundleNames = getSharedResourceBundleNames();
      ResourceBundle resourceBundle = getResourceBundle(sharedResourceBundleNames, locale);
      if (resourceBundle.containsKey(key)) {
        return resourceBundle.getString(key);
      } else if (!DEFAULT_CROWDIN_LOCALE.equals(locale)) {
        resourceBundle = getResourceBundle(sharedResourceBundleNames, DEFAULT_CROWDIN_LOCALE);
        if (resourceBundle.containsKey(key)) {
          return resourceBundle.getString(key);
        }
      }
      return null;
    } finally {
      Thread.currentThread().setContextClassLoader(contextClassLoader);
    }
  }

  public ResourceBundleData createResourceBundleDataInstance() {
    return new ResourceBundleData();
  }

  protected boolean isClasspathResource(String name) {
    if (classpathResources_ == null)
      return false;
    for (int i = 0; i < classpathResources_.size(); i++) {
      String pack = classpathResources_.get(i);
      if (name.startsWith(pack))
        return true;
    }
    return false;
  }

  protected void initResources(String baseName, Locale locale, ClassLoader cl) {
    String name = baseName.replace('.', '/');
    try {
      Locale defaultLocale = DEFAULT_CROWDIN_LOCALE;
      String language = locale.getLanguage();
      String country = locale.getCountry();
      String variant = locale.getVariant();

      String content = getResourceBundleContent(name, locale, defaultLocale, cl);
      if (content != null) {
        // save the content
        ResourceBundleData data = new ResourceBundleData();
        data.setName(baseName);
        data.setLanguage(language);
        data.setCountry(country);
        data.setVariant(variant);
        data.setData(content);
        saveResourceBundle(data);
      }
    } catch (Exception ex) {
      LOG.error("Error while reading the resource bundle : " + baseName, ex);
    }
  }

  protected String getResourceBundleContent(String name, String language, String defaultLang, ClassLoader cl)
                                                                                                              throws Exception {
    String fileName = null;
    try {
      cl = new PropertiesClassLoader(cl, true);
      fileName = name + "_" + language + ".properties";
      URL url = cl.getResource(fileName);
      if (url == null && defaultLang.equals(language)) {
        url = cl.getResource(name + ".properties");
      }
      if (url != null) {
        InputStream is = url.openStream();
        try {
          byte[] buf = IOUtil.getStreamContentAsBytes(is);
          return new String(buf, "UTF-8");
        } finally {
          try {
            is.close();
          } catch (IOException e) {
            // Do nothing
          }
        }
      }
    } catch (Exception e) {
      throw new Exception("Error while reading the file: " + fileName, e);
    }
    return null;
  }

  /**
   * This method is used for country support
   *
   * @param baseName
   * @param locale
   * @param defaultLocale
   * @param cl
   * @return
   */
  protected String getResourceBundleContent(String baseName, Locale locale, Locale defaultLocale, ClassLoader cl)
                                                                                                                  throws Exception {
    List<String> candidateFiles = new ArrayList<String>();

    String language = locale.getLanguage();
    String country = locale.getCountry().toUpperCase();
    String variant = locale.getVariant();

    String defaultLanguage = defaultLocale.getLanguage();
    String defaultCountry = defaultLocale.getCountry().toUpperCase();
    String defaultVariant = defaultLocale.getVariant();

    if (variant != null && variant.length() > 0) {
      candidateFiles.add(baseName + "_" + language + "_" + country + "_" + variant + ".properties");
    }

    if (country != null && country.length() > 0) {
      candidateFiles.add(baseName + "_" + language + "_" + country + ".properties");
    }

    if (language != null && language.length() > 0) {
      candidateFiles.add(baseName + "_" + language + ".properties");
    }

    if (defaultVariant != null && defaultVariant.length() > 0) {
      candidateFiles.add(baseName + "_" + defaultLanguage + "_" + defaultCountry + "_" + defaultVariant + ".properties");
    }

    if (defaultCountry != null && defaultCountry.length() > 0) {
      candidateFiles.add(baseName + "_" + defaultLanguage + "_" + defaultCountry + ".properties");
    }

    if (defaultLanguage != null && defaultLanguage.length() > 0) {
      candidateFiles.add(baseName + "_" + defaultLanguage + ".properties");
    }

    candidateFiles.add(baseName + ".properties");

    // fallback to en locale help to remove ant script that generates base
    // properties file
    candidateFiles.add(baseName + "_" + Locale.ENGLISH.getLanguage() + ".properties");

    cl = new PropertiesClassLoader(cl, true);
    String fileName = null;

    try {
      URL url = null;
      for (String candidateFile : candidateFiles) {
        url = cl.getResource(candidateFile);
        if (url != null) {
          fileName = candidateFile;
          break;
        }
      }

      if (url != null) {
        InputStream is = url.openStream();
        try {
          byte[] buf = IOUtil.getStreamContentAsBytes(is);
          return new String(buf, "UTF-8");
        } finally {
          try {
            is.close();
          } catch (IOException e) {
            // Do nothing
          }
        }
      }
    } catch (Exception ex) {
      throw new Exception("Error while reading the file: " + fileName, ex);
    }

    return null;

  }

  protected List<String> getInitResources_() {
    return initResources_;
  }

  protected void loadResourcesForLocale(Locale locale) {
    for (String resource : getInitResources_()) {
      initResources(resource, locale, cl);
    }
    localeList_.add(locale.toLanguageTag());
  }

  /**
   * Invalidate an entry in the cache at this level. Normally this is called by
   * the subclass.
   *
   * @param name the bundle name
   */
  protected final void invalidate(String name) {
    cache_.remove(name);
  }

  public ResourceBundle getResourceBundle(String name, Locale locale, ClassLoader cl) {
    if (IdentityResourceBundle.MAGIC_LANGUAGE.equals(locale.getLanguage())) {
      return IdentityResourceBundle.getInstance();
    }

    String country = locale.getCountry();
    String variant = locale.getVariant();
    String id;
    if (variant != null && variant.length() > 0) {
      id = name + "_" + locale.getLanguage() + "_" + country + "_" + variant;
    } else if (country != null && country.length() > 0) {
      id = name + "_" + locale.getLanguage() + "_" + locale.getCountry();
    } else {
      id = name + "_" + locale.getLanguage();
    }

    boolean isClasspathResource = isClasspathResource(name);
    boolean isCacheable = !isClasspathResource || !PropertyManager.isDevelopping();
    if (isCacheable && isClasspathResource) {
      // Avoid naming collision
      id += "_" + cl.getClass() + "_" + System.identityHashCode(cl);
    }

    // Case 1: ResourceBundle of portlets, standard java API is used
    if (isClasspathResource) {
      ResourceBundleFromCPContext ctx = new ResourceBundleFromCPContext(name, locale, cl);
      ResourceBundle result;
      // Cache classpath resource bundle while running portal in non-dev mode
      if (isCacheable) {
        result = getFutureCache().get(ctx, id);
      } else {
        result = ctx.get(id);
      }

      if (ctx.e != null) {
        // Throw the RuntimeException if it occurs to remain compatible with the
        // old behavior
        throw ctx.e;
      } else {
        return result == ExoResourceBundle.NULL_OBJECT ? null : result;
      }
    } else {
      // Case 2: ResourceBundle of portal
      ResourceBundle resourceBundle = getFutureCache().get(new GetResourceBundleFromDbContext(name, locale), id);
      return resourceBundle == ExoResourceBundle.NULL_OBJECT ? null : resourceBundle;
    }
  }

  public ResourceBundle getResourceBundle(String[] names, Locale locale, ClassLoader cl) {
    if (IdentityResourceBundle.MAGIC_LANGUAGE.equals(locale.getLanguage())) {
      return IdentityResourceBundle.getInstance();
    }
    List<String> bundleNames = Stream.of(names).distinct().toList();
    String id = "merge:" + bundleNames.hashCode() + locale.toLanguageTag().hashCode() + cl.hashCode();
    return getFutureCache().get(new GetResourceBundleContext(bundleNames.toArray(String[]::new), locale, cl), id);
  }

  protected FutureCache<String, ResourceBundle, ResourceBundleContext> getFutureCache() {
    if (futureCache_ == null) {
      synchronized (this) {
        if (futureCache_ == null) {
          futureCache_ = new FutureExoCache<String, ResourceBundle, ResourceBundleContext>(loader_, cache_) {
            @Override
            public ResourceBundle get(String key) {
              ResourceBundle resourceBundle = super.get(key);
              return resourceBundle == ExoResourceBundle.NULL_OBJECT ? null : resourceBundle;
            }
          };
        }
      }
    }
    return futureCache_;
  }

  protected abstract ResourceBundle getResourceBundleFromDb(String id, ResourceBundle parent, Locale locale) throws Exception;

  /**
   * Generic class defining a context needed to get a ResourceBundle
   */
  private abstract static class ResourceBundleContext {
    /**
     * Get the resource bundle corresponding to the context
     */
    abstract ResourceBundle get(String id);
  }

  /**
   * The class defining the context required to load a ResourceBundle from
   * classpath
   */
  private class ResourceBundleFromCPContext extends ResourceBundleContext {
    private final String      name;

    private final Locale      locale;

    private final ClassLoader cl;

    private RuntimeException  e;

    public ResourceBundleFromCPContext(String name, Locale locale, ClassLoader cl) {
      this.name = name;
      this.locale = locale;
      this.cl = cl;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    ResourceBundle get(String id) {
      ResourceBundle parent = null, result = null;
      try {
        Locale defaultLocale = DEFAULT_CROWDIN_LOCALE;

        String rootId = name + "_" + defaultLocale.getLanguage();
        parent = getContent(rootId, null, defaultLocale);
        result = getContent(name, parent, defaultLocale);
      } catch (Exception e) {
        this.e = new RuntimeException(e);
      }

      if (result != null) {
        return result;
      } else {
        return parent == null ? ExoResourceBundle.NULL_OBJECT : parent;
      }
    }

    private ResourceBundle getContent(String _name, ResourceBundle parent, Locale defaultLocale) throws Exception {
      String content = getResourceBundleContent(_name.replace('.', '/'), locale, defaultLocale, cl);
      if (content != null) {
        ResourceBundleData data = new ResourceBundleData();
        data.setName(_name);
        data.setLanguage(locale.getLanguage());
        data.setCountry(locale.getCountry());
        data.setVariant(locale.getVariant());
        data.setData(content);

        return new ExoResourceBundle(data, parent);
      }
      return null;
    }
  }

  /**
   * The class defining the context required to load a ResourceBundle thanks to
   * the method
   * <code>getResourceBundleFromDb(String id, ResourceBundle parent, Locale locale)</code>
   */
  private class GetResourceBundleFromDbContext extends ResourceBundleContext {
    private final String name;

    private final Locale locale;

    public GetResourceBundleFromDbContext(String name, Locale locale) {
      this.name = name;
      this.locale = locale;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    ResourceBundle get(String id) {
      ResourceBundle res = null;
      // local resources is not yet loaded
      if (!localeList_.contains(locale.toLanguageTag())) {
        // force the load of local resources
        loadResourcesForLocale(locale);
      }
      try {
        String rootId = name + "_" + DEFAULT_CROWDIN_LANGUAGE;
        ResourceBundle parent = getResourceBundleFromDb(rootId, null, locale);
        res = getResourceBundleFromDb(id, parent, locale);
        if (res == null) {
          res = parent;
        }
      } catch (Exception ex) {
        LOG.error("Error: " + id, ex);
      }
      return res;
    }
  }

  /**
   * The class defining the context required to load a ResourceBundle thanks to
   * the method
   * <code>getResourceBundle(String[] name, Locale locale, ClassLoader cl)</code>
   */
  private class GetResourceBundleContext extends ResourceBundleContext {
    private final String[]    name;

    private final Locale      locale;

    private final ClassLoader cl;

    public GetResourceBundleContext(String[] name, Locale locale, ClassLoader cl) {
      this.name = name;
      this.locale = locale;
      this.cl = cl;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    ResourceBundle get(String id) {
      MapResourceBundle outputBundled = null;
      try {
        outputBundled = new MapResourceBundle(locale);
        for (int i = 0; i < name.length; i++) {
          // local resources is not yet loaded
          if (!localeList_.contains(locale.toLanguageTag())) {
            // force the load of local resources
            loadResourcesForLocale(locale);
          }
          ResourceBundle temp = getResourceBundle(name[i], locale, cl);
          if (temp != null) {
            outputBundled.merge(temp);
            continue;
          }
          LOG.warn("Cannot load and merge the bundle: " + name[i]);
        }
        outputBundled.resolveDependencies();
      } catch (Exception ex) {
        LOG.error("Cannot load and merge the bundle: " + id, ex);
      }
      return outputBundled;
    }
  }
}
