/*
 * Copyright 2002-2012 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */

package de.ivu.elect.jee.faces.messagehelper;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.Nullable;

public class ResourceBundleMessageSource extends AbstractMessageSource {

  private static final long serialVersionUID = 1L;

  private String[] basenames = new String[0];

  private ClassLoader bundleClassLoader;

  private ClassLoader beanClassLoader = getDefaultClassLoader();

  public static ClassLoader getDefaultClassLoader() {
    ClassLoader cl = null;
    try {
      cl = Thread.currentThread().getContextClassLoader();
    } catch (Throwable ex) {
      // Cannot access thread context ClassLoader - falling back to system class loader...
    }
    if (cl == null) {
      // No thread context class loader -> use class loader of this class.
      cl = ResourceBundleMessageSource.class.getClassLoader();
    }
    return cl;
  }

  /**
   * Set a single basename, following {@link java.util.ResourceBundle} conventions: essentially, a fully-qualified
   * classpath location. If it doesn't contain a package qualifier (such as <code>org.mypackage</code>), it will be
   * resolved from the classpath root.
   * <p/>
   * Messages will normally be held in the "/lib" or "/classes" directory of a web application's WAR structure. They can
   * also be held in jar files on the class path.
   * <p/>
   * Note that ResourceBundle names are effectively classpath locations: As a consequence, the JDK's standard
   * ResourceBundle treats dots as package separators. This means that "test.theme" is effectively equivalent to
   * "test/theme", just like it is for programmatic <code>java.util.ResourceBundle</code> usage.
   *
   * @see #setBasenames
   * @see java.util.ResourceBundle#getBundle(String)
   */
  public void setBasename(String basename) {
    setBasenames(basename);
  }

  /**
   * Set an array of basenames, each following {@link java.util.ResourceBundle} conventions: essentially, a
   * fully-qualified classpath location. If it doesn't contain a package qualifier (such as <code>org.mypackage</code>),
   * it will be resolved from the classpath root.
   * <p/>
   * The associated resource bundles will be checked sequentially when resolving a message code. Note that message
   * definitions in a <i>previous</i> resource bundle will override ones in a later bundle, due to the sequential
   * lookup.
   * <p/>
   * Note that ResourceBundle names are effectively classpath locations: As a consequence, the JDK's standard
   * ResourceBundle treats dots as package separators. This means that "test.theme" is effectively equivalent to
   * "test/theme", just like it is for programmatic <code>java.util.ResourceBundle</code> usage.
   *
   * @see #setBasename
   * @see java.util.ResourceBundle#getBundle(String)
   */
  public void setBasenames(String... basenames) {
    if (basenames == null) {
      this.basenames = new String[0];
    } else {
      this.basenames = new String[basenames.length];

      for (int i = 0; i < basenames.length; i++) {
        this.basenames[i] = basenames[i].trim();
      }
    }
  }

  public void setBundleClassLoader(ClassLoader classLoader) {
    this.bundleClassLoader = classLoader;
  }

  /**
   * Return the ClassLoader to load resource bundles with.
   * <p/>
   * Default is the containing BeanFactory's bean ClassLoader.
   *
   * @see #setBundleClassLoader
   */
  protected ClassLoader getBundleClassLoader() {
    return this.bundleClassLoader == null ? this.beanClassLoader : this.bundleClassLoader;
  }

  public void setBeanClassLoader(ClassLoader classLoader) {
    this.beanClassLoader = classLoader == null ? getDefaultClassLoader() : classLoader;
  }

  /**
   * Resolves the given message code as key in the registered resource bundles, returning the value found in the bundle
   * as-is (without MessageFormat parsing).
   */
  @Override
  protected String resolveCodeWithoutArguments(String code, Locale locale) {
    String result = null;
    for (int i = 0; result == null && i < this.basenames.length; i++) {
      ResourceBundle bundle = getResourceBundle(this.basenames[i], locale);
      if (bundle != null) {
        result = getStringOrNull(bundle, code);
      }
    }
    return result;
  }

  /**
   * Resolves the given message code as key in the registered resource bundles, using a cached MessageFormat instance
   * per message code.
   */
  @Nullable
  @Override
  protected MessageFormat resolveCode(String code, Locale locale) {
    MessageFormat messageFormat = null;
    for (int i = 0; messageFormat == null && i < this.basenames.length; i++) {
      ResourceBundle bundle = getResourceBundle(this.basenames[i], locale);
      if (bundle != null) {
        messageFormat = getMessageFormat(bundle, code, locale);
      }
    }
    return messageFormat;
  }

  private final ConcurrentHashMap<String, ConcurrentHashMap<Locale, ResourceBundle>> resourceBundles = new ConcurrentHashMap<>();

  @Nullable
  protected ResourceBundle getResourceBundle(String basename, Locale locale) {
    ConcurrentHashMap<Locale, ResourceBundle> localeMap = this.resourceBundles.get(basename);
    ResourceBundle bundle;
    if (localeMap == null) {
      ConcurrentHashMap<Locale, ResourceBundle> newLocaleMap = new ConcurrentHashMap<>();
      localeMap = resourceBundles.putIfAbsent(basename, newLocaleMap);
      if (localeMap == null) {
        localeMap = newLocaleMap;
      }
    }

    bundle = localeMap.get(locale);
    if (bundle == null) {
      ResourceBundle newResourceBundle;
      try {
        newResourceBundle = doGetBundle(basename, locale);
      } catch (MissingResourceException ex) {

        // Assume bundle not found
        // -> do NOT throw the exception to allow for checking parent message source.
        return null;
      }

      bundle = localeMap.putIfAbsent(locale, newResourceBundle);
      if (bundle == null) {
        bundle = newResourceBundle;
      }
    }
    return bundle;
  }

  /**
   * Obtain the resource bundle for the given basename and Locale.
   *
   * @param basename the basename to look for
   * @param locale  the Locale to look for
   * @return the corresponding ResourceBundle
   * @throws MissingResourceException if no matching bundle could be found
   * @see java.util.ResourceBundle#getBundle(String, java.util.Locale, ClassLoader)
   * @see #getBundleClassLoader()
   */
  protected ResourceBundle doGetBundle(String basename, Locale locale) {

    // ELECT-3395
    //
    // Es soll verhindert werden, dass das Runtime nach einem Bundle gemäß der JVM-Lokalisierung sucht, bevor
    // das Suffix-lose Bundle berücksichtigt worden ist.
    //

    Control control = new Control() {

      @Override
      public List<Locale> getCandidateLocales(String baseName, Locale locale) {
        if ("".equals(locale.getLanguage())) {
          return Arrays.asList(locale);
        }
        return Arrays.asList(locale, new Locale(""));
      }
    };

    return ResourceBundle.getBundle(basename, locale, getBundleClassLoader(), control);
  }

  private final ConcurrentHashMap<ResourceBundle, ConcurrentHashMap<String, ConcurrentHashMap<Locale, MessageFormat>>> bundleMessageFormats = new ConcurrentHashMap<>();

  @Nullable
  protected MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale) throws MissingResourceException {

    ConcurrentHashMap<String, ConcurrentHashMap<Locale, MessageFormat>> codeMap = this.bundleMessageFormats.get(bundle);

    ConcurrentHashMap<Locale, MessageFormat> localeMap = null;
    if (codeMap != null) {
      localeMap = codeMap.get(code);
      if (localeMap != null) {
        MessageFormat result = localeMap.get(locale);
        if (result != null) {
          return result;
        }
      }
    }

    String msg = getStringOrNull(bundle, code);
    if (msg != null) {
      if (codeMap == null) {
        ConcurrentHashMap<String, ConcurrentHashMap<Locale, MessageFormat>> newCodeMap = new ConcurrentHashMap<>();
        codeMap = this.bundleMessageFormats.putIfAbsent(bundle, newCodeMap);
        if (codeMap == null) {
          codeMap = newCodeMap;
        }
      }

      if (localeMap == null) {
        ConcurrentHashMap<Locale, MessageFormat> newLocaleMap = new ConcurrentHashMap<>();
        localeMap = codeMap.putIfAbsent(code, newLocaleMap);
        if (localeMap == null) {
          localeMap = newLocaleMap;
        }
      }

      MessageFormat newMessageFormat = createMessageFormat(msg, locale);
      MessageFormat result = localeMap.putIfAbsent(locale, newMessageFormat);
      if (result == null) {
        result = newMessageFormat;
      }
      return result;
    }

    return null;
  }

  @Nullable
  private String getStringOrNull(ResourceBundle bundle, String key) {
    // NOTE ELECT-2798: NICHT über die MissingResourceException gehen, sondern ExPLIZIT mit containsKey abfragen!
    if (bundle.containsKey(key)) {
      return bundle.getString(key);
    }
    return null;
  }



  /**
   * Show the configuration of this MessageSource.
   */
  @Override
  public String toString() {
    return getClass().getName() + ": basenames=[" + new ArrayList<>(Arrays.asList(this.basenames)) + "]";
  }

}
