/*
 * (C) 2004 - Geotechnical Software Services
 * 
 * This code 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 code 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., 59 Temple Place - Suite 330, Boston, 
 * MA  02111-1307, USA.
 */
package no.geosoft.cc.locale;



import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.ref.WeakReference;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;



/**
 * Class for holding current locale information, and notify client
 * application about locale changes. Used for dynamic (or static)
 * locale updates and language translations during a program session.
 * <p>
 * For language dependent user interfaces, put translation files into
 * the packages where reguired. The files are named as follows:
 * <tt>Messages_xx_XX.properties</tt> where <em>xx</em> is the laguage
 * identifier and <em>XX</em> is the country identifier, for instance
 *
 * <pre>
 *   Messages_en_US.properties  // american
 *   Messages_fr_FR.properties  // french
 *   Messages_ch_CH.properties  // chineese
 * </pre>
 *
 * and so on. These are ordinary properties files containing language
 * translations, for instance (french):
 *
 * <pre>
 *   Close = Fermer
 *   New   = Nouveau
 *   Help  = Aide
 * </pre>
 *
 * and (american):
 *
 * <pre>
 *   Close = Close
 *   New   = New
 *   Help  = Help
 * </pre>
 *
 * <p>
 * A locale aware GUI will use the locale manager like this:
 *
 * <pre>
 *   JButton helpButton = new JButton();
 *   :
 *   helpButton.setText (localeManager.getText ("Help"));
 * </pre>
 *
 * If the latter is done inside a localeChanged callback, the GUI will
 * update itself dynamically if locale is changed during a session.
 *
 * @author <a href="mailto:info@geosoft.no">GeoSoft</a>
 */   
public class LocaleManager
{
  private static LocaleManager instance_ = null;
  
  private Locale           defaultLocale_;
  private Locale           currentLocale_;
  private HashMap          resourceBundles_;  // String -> ResourceBundle
  private Collection       listeners_;        // of WeakReference (LocaleListener)

  
  
  /**
   * Create the locale manager instance.
   */
  private LocaleManager()
  {
    instance_ = this;

    currentLocale_   = null;
    defaultLocale_   = null;
    resourceBundles_ = new HashMap();
    listeners_       = null;
    
    // Set locale for this JVM. This is independent of the default
    // locale of the application.
    Locale.setDefault (new Locale ("en", "US"));

    setDefaultLocale ("en", "US");    
    setLocale ("en", "US");
  }



  /**
   * Return the sole instance of this class.
   * 
   * @return  The sole locale manager instance.
   */
  public static LocaleManager getInstance()
  {
    if (instance_ == null) 
      instance_ = new LocaleManager();
    return instance_;
  }


  
  /**
   * Set the default locale. If a translation is not found for a key
   * in the current locale, the current locale is used instead.
   * 
   * @param language  Language of default locale.
   * @param country   Country of default locale.
   */
  public void setDefaultLocale (String language, String country)
  {
    defaultLocale_ = new Locale (language, country);    
  }

  

  /**
   * Set current locale.
   * 
   * @param locale  New current locale.
   */
  public void setLocale (Locale locale)
  {
    setLocale (locale.getLanguage(), locale.getCountry());
  }


  
  /**
   * Return current locale.
   * 
   * @return  Current locale.
   */
  public Locale getLocale()
  {
    return currentLocale_;
  }
  


  /**
   * Set the current locale, and notify listeners about the change.
   * 
   * @param language  Language of new locale.
   * @param country   Country of new locale.
   */
  public void setLocale (String language, String country)
  {
    // Leave here if no change
    if (currentLocale_ != null) {
      if (language.equals (currentLocale_.getLanguage()) &&
          country.equals (currentLocale_.getCountry()))
        return;
    }
    
    // Create the new locale
    currentLocale_ = new Locale (language, country);    

    // Clear resource bundles
    resourceBundles_.clear();

    // Call listeners
    if (listeners_ != null) {
      for (Iterator i = listeners_.iterator(); i.hasNext(); ) {
        WeakReference reference = (WeakReference) i.next();
        LocaleListener listener = (LocaleListener) reference.get();
        if (listener == null)
          i.remove();
        else
          listener.localeChanged();
      }
    }
  }

  

  /**
   * Find resource bundle of specified name.
   * 
   * @param bundleName  Name of bundle to find.
   * @return            Requested resource bundle (or null if not found).
   */
  private ResourceBundle findResourceBundle (String bundleName)
  {
    ResourceBundle resourceBundle;

    if (!resourceBundles_.containsKey (bundleName)) {
      try {
        resourceBundle = ResourceBundle.getBundle (bundleName,
                                                   currentLocale_);
      }
      catch (MissingResourceException exception1) {
        try {
          resourceBundle = ResourceBundle.getBundle (bundleName,
                                                     defaultLocale_);
        }
        catch (MissingResourceException exception2) {
          resourceBundle = null;
        }
      }

      if (resourceBundle != null)
        resourceBundles_.put (bundleName, resourceBundle);
    }

    resourceBundle = (ResourceBundle) resourceBundles_.get (bundleName);
    return resourceBundle;
  }
  
  
  
  /**
   * Returned localized text for specified text. Search resource within
   * specified package.
   * 
   * @param packageName  Name of package to locate resource.
   * @param tag          Text tag.
   * @return             Localized text.
   */
  private String getText (String packageName, String tag)
  {
    try {
      ResourceBundle resourceBundle = (ResourceBundle) resourceBundles_.
                                      get (packageName);

      if (resourceBundle == null) {
        String bundleName = packageName + ".Messages";
        resourceBundle = ResourceBundle.getBundle (bundleName,
                                                   currentLocale_);
        resourceBundles_.put (packageName, resourceBundle);
      }
      
      return resourceBundle.getString (tag);
    }
    catch (Exception exception) {
      // The bundle of the value was not found; return tag instead
      return tag;
    }
  }
  
  

  /**
   * Return localized text for a specified tag. The text is looked up
   * in the resource bundle of the package of the caller.
   * 
   * @param tag  Tag of text to locate.
   * @return     Localized text of tag.
   */
  public String getText (String tag)
  {
    // Find calling class
    StringWriter stringWriter = new StringWriter();
    new Throwable().printStackTrace (new PrintWriter(stringWriter));
    String callStack = stringWriter.toString();
    int pos1 = callStack.indexOf ("at ");
    pos1 = callStack.indexOf ("at ", pos1 + 2) + 3;
    int pos2 = callStack.indexOf ("(", pos1);
    String line = callStack.substring (pos1, pos2);
    line = line.substring (0, line.lastIndexOf ("."));
    String packageName = line.substring (0, line.lastIndexOf ("."));    

    return getText (packageName, tag);
  }


  
  /**
   * Add a locale listener. The listener is notified when the current
   * locale of the LocaleManager is changed.
   * 
   * @param localeListener  Listener to add.
   */
  public void addLocaleListener (LocaleListener localeListener)
  {
    if (listeners_ == null)
      listeners_ = new ArrayList();

    // Check to see if it is there already
    for (Iterator i = listeners_.iterator(); i.hasNext(); ) {
      WeakReference reference = (WeakReference) i.next();
      LocaleListener listener = (LocaleListener) reference.get();
      if (listener == localeListener)
        return;
    }

    // Add the listener
    listeners_.add (new WeakReference (localeListener));
  }



  /**
   * Remove a locale listener.
   * 
   * @param localeListener  Locale listener to remove.
   */
  public void removeLocaleListener (LocaleListener localeListener)
  {
    if (listeners_ == null) return;

    for (Iterator i = listeners_.iterator(); i.hasNext(); ) {
      WeakReference reference = (WeakReference) i.next();
      LocaleListener listener = (LocaleListener) reference.get();
      if (listener == localeListener) {
        i.remove();
        break;
      }
    }
  }


  
  /**
   * Get default date format for the current locale.
   * 
   * @param style  One of DateFormat.SHORT, MEDIUM or LONG.
   * @return       Date format for current locale.
   */
  public DateFormat getDateFormat (int style)
  {
    return DateFormat.getDateInstance (style, currentLocale_);
  }


  
  /**
   * Get default time format for the current locale.
   * 
   * @param style  One of DateFormat.SHORT, MEDIUM or LONG.
   * @return       Time format for current locale.
   */
  public DateFormat getTimeFormat (int style)
  {
    DateFormat dateFormat = DateFormat.getTimeInstance (style, currentLocale_);

		// TODO: Check this
    String pattern = ((SimpleDateFormat) dateFormat).toPattern();
    if (style == DateFormat.LONG){
      int ind = pattern.lastIndexOf('s');
      String newPattern = null;
      if (ind > 0){
        newPattern = pattern.substring(0,ind+1)+".SSS";
        if (ind+1 < pattern.length()){
          newPattern += pattern.substring(ind+1,pattern.length());
        }
      }
      pattern = newPattern;
    } 				

    if ((pattern.indexOf("h") == pattern.lastIndexOf("h")) &&
        (pattern.indexOf("h") != -1)) {
      pattern = pattern.replaceAll("h","HH");
    }
    else {
      pattern = pattern.replaceAll("h","H");
    }

    pattern = pattern.replaceAll("a",""); 	
    pattern = pattern.replaceAll("z",""); 	
    pattern = pattern.trim();

    ((SimpleDateFormat)dateFormat).applyPattern (pattern);							
      
    return dateFormat;
  }
  

  
  /**
   * Get defaut date/time format for current locale.
   * 
   * @param dateStyle  One of DateFormat.SHORT, MEDIUM or LONG.
   * @param timeStyle  One of DateFormat.SHORT, MEDIUM or LONG.
   * @return           Date/time format for the currnt locale.
   */
  public DateFormat getDateTimeFormat (int dateStyle, int timeStyle)
  {
  	DateFormat dateFormat = DateFormat.getDateTimeInstance (dateStyle, timeStyle, currentLocale_);
  	
    String datePattern = ((SimpleDateFormat)DateFormat.getDateInstance(dateStyle,currentLocale_)).toPattern();	
    DateFormat timeFormat = getTimeFormat(timeStyle);
    if (timeFormat instanceof SimpleDateFormat){
      String timePattern = ((SimpleDateFormat)timeFormat).toPattern();	
      String pattern = datePattern + " " + timePattern;		
      ((SimpleDateFormat)dateFormat).applyPattern(pattern);			
    }
		
    return dateFormat;
  }
  

  
  /**
   * Return default localized text reprentation of the specified date.
   * 
   * @param date  Date to format.
   * @return      String representation of specified date.
   */
  public String getDate (Date date)
  {
    return getDate (date, DateFormat.SHORT);
  }
  

  /**
   * Return default localized text reprentation of the specified date.
   * 
   * @param date   Date to format.
   * @param style  DateFormat.SHORT, MEDIUM or LONG
   * @return       String representation of specified date.
   */
  public String getDate (Date date, int style)
  {
    if (date == null) return "";
    DateFormat dateFormat = getDateFormat (style);
    return dateFormat.format (date);
  }


  
  /**
   * Return default localized text representation of the specified time.
   * 
   * @param date   Time to format.
   * @param style  DateFormat.SHORT, MEDIUM or LONG
   * @return       String representation of specified time.
   */
  public String getTime (Date date, int style)
  {
    if (date == null) return "";
    DateFormat dateFormat = getTimeFormat (style);
    return dateFormat.format (date);
  }


  
  /**
   * Return default localized text representation of the specified time.
   * 
   * @param date  Time to format.
   * @return      String representation of specified time.
   */
  public String getTime (Date date)
  {
    return getTime (date, DateFormat.SHORT);
  }


  
  /**
   * Return default localized text representation of the specified date/time.
   * 
   * @param date       Date/time to format.
   * @param dateStyle  One of DateFormat.SHORT, MEDIUM or LONG.
   * @param timeStyle  One of DateFormat.SHORT, MEDIUM or LONG.   
   * @return           String representation of specified date/time.
   */
  public String getDateTime (Date date, int dateStyle, int timeStyle)
  {
    if (date == null) return "";
    DateFormat dateFormat = getDateTimeFormat (dateStyle, timeStyle);
    return dateFormat.format (date);
  }



  /**
   * Return default localized text representation of the specified date/time.
   * 
   * @param date  Date/time to format.
   * @return      String representation of specified date/time.
   */
  public String getDateTime (Date date)
  {
    return getDateTime (date, DateFormat.SHORT, DateFormat.SHORT);
  }
}