/*
 * Created on 29.11.2003
 * Copyright (C) 2003, 2004, 2005, 2006 Aelitis, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * 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 General Public License for more details.
 * You should have received a copy of the GNU 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.
 *
 * AELITIS, SAS au capital de 46,603.30 euros
 * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
 * 
 */
package org.gudy.azureus2.core3.internat;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.util.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.*;

/**
 * @author Rene Leonhardt
 */
public class 
IntegratedResourceBundle 
	extends ResourceBundle 
{
	private static final boolean DEBUG = false;

	private static final Object	NULL_OBJECT = new Object();
	
	private static final Map	bundle_map = new WeakHashMap();

	private static TimerEventPeriodic	compact_timer;		
	
	protected static boolean upper_case_enabled;
	
	static{
		COConfigurationManager.addAndFireParameterListener(
			"label.lang.upper.case",
			new ParameterListener()
			{
				public void 
				parameterChanged(
					String name )
				{
					upper_case_enabled = COConfigurationManager.getBooleanParameter( name, false );
				}
			});
	}
	
	protected static void
	resetCompactTimer()
	{
		synchronized( bundle_map ){

			if ( compact_timer == null && System.getProperty("transitory.startup", "0").equals("0")){
								
				compact_timer = SimpleTimer.addPeriodicEvent( 
					"IRB:compactor",
					60*1000,
					new TimerEventPerformer()
					{
						public void 
						perform(
							TimerEvent event )
						{	
							synchronized( bundle_map ){
					
								Iterator it = bundle_map.keySet().iterator();
					
								boolean	did_something = false;
					
								while( it.hasNext()){
					
									IntegratedResourceBundle	rb = (IntegratedResourceBundle)it.next();
					
									if (DEBUG) {
										System.out.println("Compact RB " + rb.getString());
									}

									if ( rb.compact()){
					
										did_something	= true;
									}
									if (DEBUG) {
										System.out.println("        to " + rb.getString());
									}
								}
					
								if ( !did_something ){
					
									compact_timer.cancel();
				
									compact_timer	= null;
								}
							}
						}
					});
			}
		}
	}
	
	private Locale	locale;
	private boolean	is_message_bundle;

	private Map	messages;
	private Map	used_messages;
	private List null_values;
	
	private boolean		messages_dirty;
	private int			clean_count	= 0;
	private boolean		one_off_discard_done;
	
	private File		scratch_file_name;
	private InputStream	scratch_file_is;

	private final int initCapacity;
	
	private Map<String,String>	added_strings;
	
	public 
	IntegratedResourceBundle(
		ResourceBundle 		main, 
		Map 				localizationPaths) 
	{
		this( main, localizationPaths, null, 10 );
	}

	public 
	IntegratedResourceBundle(
		ResourceBundle 		main, 
		Map 				localizationPaths,
		int					initCapacity) 
	{
		this( main, localizationPaths, null, initCapacity );
	}

	public 
	IntegratedResourceBundle(
		ResourceBundle 		main, 
		Map 				localizationPaths,
		Collection 			resource_bundles,
		int					initCapacity)  
	{
		this( main, localizationPaths, resource_bundles, initCapacity, false );
	}
	
	public 
	IntegratedResourceBundle(
		ResourceBundle 		main, 
		Map 				localizationPaths,
		Collection 			resource_bundles,
		int					initCapacity,
		boolean				isMessageBundle )
	{
		this.initCapacity 	= initCapacity;
		is_message_bundle	= isMessageBundle;
		
		messages = new LightHashMap(initCapacity);

		locale = main.getLocale();

			// use a somewhat decent initial capacity, proper calculation would require java 1.6
		
		addResourceMessages( main, isMessageBundle );
		
		synchronized (localizationPaths)
		{
			for (Iterator iter = localizationPaths.keySet().iterator(); iter.hasNext();){
				String localizationPath = (String) iter.next();
				ClassLoader classLoader = (ClassLoader) localizationPaths.get(localizationPath);

				addPluginBundle(localizationPath, classLoader);
			}
		}

		if (resource_bundles != null) {
			synchronized (resource_bundles)
			{
				for (Iterator itr = resource_bundles.iterator(); itr.hasNext();) {
					addResourceMessages((ResourceBundle)itr.next());
				}
			}

		}
		
		used_messages = new LightHashMap( messages.size());
		
		synchronized( bundle_map ){
			
			bundle_map.put( this, NULL_OBJECT );
			
			resetCompactTimer();
		}
		//System.out.println("IRB Size = " + messages.size() + "/cap=" + initCapacity + ";" + Debug.getCompressedStackTrace());
	}

	public Locale getLocale() 
	{
		return locale;
	}

	private Map
	getMessages()
	{
		return( loadMessages());
	}
	
	public Enumeration 
	getKeys() 
	{
		new Exception("Don't call me, call getKeysLight").printStackTrace();
		
		Map m = loadMessages();
		
		return( new Vector( m.keySet()).elements());
	}
	
	protected Iterator
	getKeysLight()
	{
		Map m = new LightHashMap(loadMessages());
		
		return( m.keySet().iterator());
	}

	/**
	 * Gets a string, using default if key doesn't exist.  Skips
	 * throwing MissingResourceException when key doesn't exist, which saves
	 * some CPU cycles
	 * 
	 * @param key
	 * @param def
	 * @return
	 *
	 * @since 3.1.1.1
	 */
  public String getString(String key, String def) {
		String s = (String) handleGetObject(key);
		if (s == null) {
			if (parent != null) {
				s = parent.getString(key);
			}
			if (s == null) {
				return def;
			}
		}
		return s;
	}

	
	protected Object 
	handleGetObject(
		String key )
	{
		Object	res;
		
		synchronized( bundle_map ){
		
			res = used_messages.get( key );
		}
		
		Integer keyHash = null;
		if (null_values != null) {
			keyHash = new Integer(key.hashCode());
  		int index = Collections.binarySearch(null_values, keyHash);
  		if (index >= 0) {
  			return null;
  		}
		}
		
		if ( res == NULL_OBJECT ){
			
			return( null );
		}
		
		if ( res == null ){
			
			synchronized( bundle_map ){

				loadMessages();
			
				if ( messages != null ){
					
					res = messages.get( key );
				}

				if (res == null && null_values != null) {

		  		int index = Collections.binarySearch(null_values, keyHash);
					if (index < 0) {
						index = -1 * index - 1; // best guess
					}

					if (index > null_values.size()) {
						index = null_values.size();
					}
					
					null_values.add(index, keyHash);
					
				} else {

					used_messages.put( key, res==null?NULL_OBJECT:res );
				}
					
				clean_count	= 0;
				
				resetCompactTimer();
			}
		}
		
		return( res );
	}
	
	public void addPluginBundle(String localizationPath, ClassLoader classLoader)
	{
		ResourceBundle newResourceBundle = null;
		try {
			if(classLoader != null)
				newResourceBundle = ResourceBundle.getBundle(localizationPath, locale ,classLoader);
			else
				newResourceBundle = ResourceBundle.getBundle(localizationPath, locale,IntegratedResourceBundle.class.getClassLoader());
		} catch (Exception e) {
			//        System.out.println(localizationPath+": no resource bundle for " +
			// main.getLocale());
			try {
				if(classLoader != null)
					newResourceBundle = ResourceBundle.getBundle(localizationPath, MessageText.LOCALE_DEFAULT,classLoader);
				else 
					newResourceBundle = ResourceBundle.getBundle(localizationPath, MessageText.LOCALE_DEFAULT,IntegratedResourceBundle.class.getClassLoader());
			} catch (Exception e2) {
				System.out.println(localizationPath + ": no default resource bundle");
				return;
			}
		}
				
		addResourceMessages(newResourceBundle, true );

		
	}
	
	public void 
	addResourceMessages(
		ResourceBundle bundle )
	{
		addResourceMessages( bundle, false );
	}
	
	public void 
	addResourceMessages(
		ResourceBundle 	bundle,
		boolean			are_messages )
	{
		boolean upper_case = upper_case_enabled && ( is_message_bundle || are_messages );
		
		synchronized (bundle_map)
		{
			loadMessages();
			
			if ( bundle != null ){
				
				messages_dirty = true;
				
				if ( bundle instanceof IntegratedResourceBundle ){
					
					if ( upper_case ){
						
						Map<String,String> m = ((IntegratedResourceBundle)bundle).getMessages();
						
						for ( Map.Entry<String,String> entry: m.entrySet()){
							
							messages.put( entry.getKey(), toUpperCase( entry.getValue()));
						}
					}else{
					
						messages.putAll(((IntegratedResourceBundle)bundle).getMessages());
					}
				}else{
					
					for (Enumeration enumeration = bundle.getKeys(); enumeration.hasMoreElements();) {
						
						String key = (String) enumeration.nextElement();
						
						if ( upper_case ){
							
							messages.put(key, toUpperCase((String)bundle.getObject(key)));
						}else{
						
							messages.put(key, bundle.getObject(key));
						}
					}
				}
			}			
		}
//		System.out.println("after addrb; IRB Size = " + messages.size() + "/cap=" + initCapacity + ";" + Debug.getCompressedStackTrace());
	}
	
	private String
	toUpperCase(
		String	str )
	{
		int	pos1 = str.indexOf( '{' );
	
		if ( pos1 == -1 ){
			
			return( str.toUpperCase( locale ));
		}
		
		int	pos = 0;
		int	len = str.length();
		
		StringBuffer result = new StringBuffer( len );
		
		while( pos < len ){
			
			if ( pos1 > pos ){
			
				result.append( str.substring( pos, pos1 ).toUpperCase( locale ));
			}
			
			if ( pos1 == len ){
				
				return( result.toString());
			}
			
			int pos2 = str.indexOf( '}', pos1 );
			
			if ( pos2 == -1 ){
				
				result.append( str.substring( pos1 ).toUpperCase( locale ));
				
				return( result.toString());
			}
			
			pos2++;
			
			result.append( str.substring( pos1, pos2 ));
			
			pos = pos2;
			
			pos1 = str.indexOf( '{', pos );
			
			if ( pos1 == -1 ){
				
				pos1 = len;
			}
		}
		
		return( result.toString());
	}
	
	protected boolean
	compact()
	{
		// System.out.println("compact " + getString() + ": cc=" + clean_count );
		
		clean_count++;
		
		if ( clean_count == 1 ){
					
			return( true );
		}
		
		if ( scratch_file_is == null || messages_dirty ){
			
			File temp_file = null;
			
			FileOutputStream	fos = null;
			
				// we have a previous cache, discard 
			
			if ( scratch_file_is != null ){
				
					// System.out.println( "discard cache file " + scratch_file_name + " for " + this );
				
				try{
					scratch_file_is.close();
										
				}catch( Throwable e ){	
					
					scratch_file_name = null;
					
				}finally{
					
					scratch_file_is = null;
				}
			}
			
			try{
				Properties props = new Properties();
				
				props.putAll( messages );
				
				if ( scratch_file_name == null ){
				
					temp_file = AETemporaryFileHandler.createTempFile();
					
				}else{
					
					temp_file = scratch_file_name;
				}
				
				fos = new FileOutputStream( temp_file );
				
				props.store( fos, "message cache" );
				
				fos.close();
				
				fos = null;
				
					// System.out.println( "wrote cache file " + temp_file + " for " + this );
				
				scratch_file_name	= temp_file;
				scratch_file_is 	= new FileInputStream( temp_file );
				
				messages_dirty		= false;
				
			}catch( Throwable e ){
				
				if ( fos != null ){
					
					try{
						fos.close();
						
					}catch( Throwable f ){
						
					}
				}
				
				if  ( temp_file != null ){
				
					temp_file.delete();
				}
			}
		}
		
		if ( scratch_file_is != null ){
			
			if ( clean_count >= 2 ){
		
				/*
				if ( messages != null ){
				
					System.out.println( "messages discarded  " + scratch_file_name + " for " + this );
				}
				*/
				
					// throw away full message map after 2 ticks
			
				messages = null;
			}
		
			if ( clean_count == 5 && !one_off_discard_done){

					// System.out.println( "used discard " + scratch_file_name + " for " + this );
				
				one_off_discard_done = true;
				
					// one off discard of used_messages to clear out any that were
					// accessed once and never again
			
				used_messages.clear();
			}
		}
		
		if ( clean_count > 5 ){
		
			Map	compact_um = new LightHashMap( used_messages.size() + 16 );
			
			compact_um.putAll( used_messages );
			
			used_messages = compact_um;
			
			return( false );
			
		}else{
		
			return( true );
		}
	}
	
	protected Map
	loadMessages()
	{
		synchronized( bundle_map ){
			
			if ( messages != null ){
				
				return( messages );
			}
			
			Map	result;
			
			if ( scratch_file_is == null ){
				
				result = new LightHashMap();
				
			}else{
			
					// System.out.println( "read cache file " + scratch_file_name + " for " + this );

				
				Properties p = new Properties();
				
				InputStream	fis = scratch_file_is;
				
				try{
									
					p.load( fis );
					
					fis.close();
					
					scratch_file_is = new FileInputStream( scratch_file_name );
					
					messages = new LightHashMap();
					
					messages.putAll( p );
					
					result = messages;
					
				}catch( Throwable e ){
					
					if ( fis != null ){
						
						try{
							fis.close();
							
						}catch( Throwable f ){
						}
					}
					
					Debug.out( "Failed to load message bundle scratch file", e );
					
					scratch_file_name.delete();
					
					scratch_file_is = null;
					
					result = new LightHashMap();
				}
			}
			
			if ( added_strings != null ){
				
				result.putAll( added_strings );
			}
			
			return( result );
		}
	}
	
	protected String
	getString()
	{
		return( locale + ": use=" + used_messages.size() + ",map=" + (messages==null?"":String.valueOf(messages.size())) 
				+ (null_values == null ? "" : ",null=" + null_values.size()) + ",added=" + (added_strings==null?"":added_strings.size()));
	}
	
	public void
	addString(String key, String value) {
		synchronized( bundle_map ){
			if ( added_strings == null ){
				
				added_strings = new HashMap<String, String>();
			}
			
			added_strings.put( key, value );
			
			if ( messages != null ){
				
				messages.put( key, value );
			}
		}
	}

	public boolean getUseNullList() {
		return null_values != null;
	}

	public void setUseNullList(boolean useNullList) {
		if (useNullList && null_values == null) {
			null_values = new ArrayList(0);
		} else if (!useNullList && null_values != null) {
			null_values = null;
		}
	}
	
	public void clearUsedMessagesMap(int initialCapacity) {
		used_messages = new LightHashMap(initialCapacity);
		if (null_values != null) {
			null_values = new ArrayList(0);
		}
	}
}
