/*
 * Created on Sep 9, 2010
 * Created by Paul Gardner
 * 
 * Copyright 2010 Vuze, Inc.  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; version 2 of the License only.
 * 
 * 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.
 */


package org.gudy.azureus2.core3.util;

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryType;
import java.util.List;

import javax.management.Notification;
import javax.management.NotificationEmitter;
import javax.management.NotificationListener;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.logging.LogAlert;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.platform.PlatformManager;
import org.gudy.azureus2.platform.PlatformManagerCapabilities;
import org.gudy.azureus2.platform.PlatformManagerFactory;

public class 
AEMemoryMonitor 
{
	private static final long MB = 1024*1024;

	private static long	max_heap_mb;
	
	protected static void
	initialise()
	{
		try{
			List<MemoryPoolMXBean> pools = ManagementFactory.getMemoryPoolMXBeans();
		
			MemoryPoolMXBean 	pool_to_monitor = null;
			long				ptm_size		= 0;
			
			long	overall_max = 0;
			
			for ( MemoryPoolMXBean pool: pools ){
			
				long pool_max = pool.getUsage().getMax();
				
				if ( pool_max > 0 ){
					
					if ( pool.getType() == MemoryType.HEAP ){
					
						overall_max += pool_max;
					}
				}
				
				if ( pool.getType() == MemoryType.HEAP && pool.isCollectionUsageThresholdSupported()){
				
					long max = pool.getUsage().getMax();
					
					if ( max > ptm_size ){
						
						pool_to_monitor = pool;
						ptm_size		= max;
					}
				}
			}
			
			max_heap_mb = (overall_max+MB-1)/MB;

			if ( pool_to_monitor != null ){

				long max = pool_to_monitor.getUsage().getMax();
				
				long threshold = max*3/4;
				
				threshold = Math.min( threshold, 5*1024*1024 );
				
				MemoryMXBean mbean = ManagementFactory.getMemoryMXBean();

				NotificationEmitter emitter = (NotificationEmitter) mbean;

				
				emitter.addNotificationListener(
					new NotificationListener()
					{
						private long	last_mb_log = Long.MAX_VALUE;
						
						private boolean increase_tried;
						
						public void 
						handleNotification(
							Notification 	notification, 
							Object 			handback ) 
						{
							MemoryPoolMXBean pool = (MemoryPoolMXBean)handback;

							long used 	= pool.getCollectionUsage().getUsed();
							long max 	= pool.getUsage().getMax();
							
							long avail = max-used;
							
							if ( avail < 0 ){
								avail = 0;
							}
							
							long	mb = (avail+MB-1)/(MB);
							
							if ( mb <= 4 ){
								
								synchronized( this ){
									
									if ( mb >= last_mb_log ){
										
										return;
									}
									
									last_mb_log = mb;
								}
								
								Runtime runtime = Runtime.getRuntime();
								
								Debug.out( "MemMon: notify triggered: pool=" + pool.getName() + 
											", used=" + used + 
											", max=" + max + 
											": runtime free=" + runtime.freeMemory() + ", tot=" + runtime.totalMemory() + ", max=" + runtime.maxMemory());

	 							Logger.logTextResource(
	 								new LogAlert(
	 									LogAlert.REPEATABLE, 
	 									LogAlert.AT_WARNING,
										"memmon.low.warning"), 
										new String[] {
	 										(mb==0?"< ":"") + DisplayFormatters.formatByteCountToKiBEtc( Math.max(1,mb)*MB, true ),
	 										DisplayFormatters.formatByteCountToKiBEtc( max_heap_mb*MB, true )});
	 							
	 							if ( mb == 1 && !increase_tried ){
	 								
	 								increase_tried = true;
	 							
	 								if ( COConfigurationManager.getBooleanParameter( "jvm.heap.auto.increase.enable", true )){

		 								PlatformManager platform = PlatformManagerFactory.getPlatformManager();
	
		 								if ( platform.hasCapability( PlatformManagerCapabilities.AccessExplicitVMOptions )){
		 									
		 									try{
		 										String[] options = platform.getExplicitVMOptions();
	
		 										long	max_mem = getJVMLongOption( options, "-Xmx" );
	
		 										if ( max_mem <= 0 ){
		 											
		 											max_mem = getMaxHeapMB()*MB;
		 										}
		 										
		 										final long HEAP_AUTO_INCREASE_MAX 	= 256*MB;
		 										final long HEAP_AUTO_INCREASE_BY	= 16*MB;
		 										
		 										if ( max_mem > 0 && max_mem < HEAP_AUTO_INCREASE_MAX ){
		 												 							 												
	 												max_mem += HEAP_AUTO_INCREASE_BY;
	 												
	 												if ( max_mem > HEAP_AUTO_INCREASE_MAX ){
	 													
	 													max_mem = HEAP_AUTO_INCREASE_MAX;
	 												}
	 												
	 												long	last_increase = COConfigurationManager.getLongParameter( "jvm.heap.auto.increase.last", 0 );
	 												
	 												if ( max_mem > last_increase ){
	 													
	 													COConfigurationManager.setParameter( "jvm.heap.auto.increase.last", max_mem );
	 															
		 												options = setJVMLongOption( options, "-Xmx", max_mem );
		 												
		 												platform.setExplicitVMOptions( options );
		 												
		 					 							Logger.logTextResource(
		 						 								new LogAlert(
		 						 									LogAlert.REPEATABLE, 
		 						 									LogAlert.AT_WARNING,
		 															"memmon.heap.auto.increase.warning"),
		 															new String[] {
		 					 											DisplayFormatters.formatByteCountToKiBEtc( max_mem, true )});
	 												}
	 											}
		 									
		 									}catch( Throwable e ){
		 										
		 										Debug.out( e );
		 									}
		 								}
	 								}
	 							}
							}
						}
					},
					null, pool_to_monitor );

				pool_to_monitor.setCollectionUsageThreshold( threshold );

			}
		}catch( Throwable e ){
		
			Debug.out( e );
		}
		
		if ( max_heap_mb == 0 ){
			
			max_heap_mb = ( Runtime.getRuntime().maxMemory()+MB-1)/MB;
		}
	}
	
	public static long
	getMaxHeapMB()
	{
		return( max_heap_mb );
	}
	
	public static  long
	getJVMLongOption(
		String[]	options,
		String		prefix )
	{		
		long	value = -1;
		
		for ( String option: options ){
			
			try{
				if ( option.startsWith( prefix )){
					
					String	val = option.substring( prefix.length());
					
					value = decodeJVMLong( val );
				}
			}catch( Throwable e ){
					
				Debug.out( "Failed to process option '" + option + "'", e );
			}
		}
		
		return( value );
	}
	
	public static  String[]
	setJVMLongOption(
		String[]	options,
		String		prefix,
		long		val )
	{
		String new_option = prefix + encodeJVMLong( val );
				
		for (int i=0;i<options.length;i++){
			
			String option = options[i];
			
			if ( option.startsWith( prefix )){
			
				options[i] = new_option;
				
				new_option = null;
			}
		}
		
		if ( new_option != null ){
		
			String[] new_options = new String[options.length+1];
		
			System.arraycopy( options, 0, new_options, 0, options.length );
			
			new_options[options.length] = new_option;
			
			options = new_options;
		}
		
		return( options );
	}
		
	public static  long
	decodeJVMLong(
		String		val )
	
		throws Exception
	{
		long	 mult = 1;
		
		char last_char = Character.toLowerCase( val.charAt( val.length()-1 ));
		
		if ( !Character.isDigit( last_char )){
			
			val = val.substring( 0, val.length()-1 );
			
			if ( last_char == 'k' ){
					
				mult	= 1024;
				
			}else if ( last_char == 'm' ){
				
				mult	= 1024*1024;
				
			}else if ( last_char == 'g' ){
				
				mult	= 1024*1024*1024;
				
			}else{
				
				throw( new Exception( "Invalid size unit '" + last_char + "'" ));
			}
		}
		
		return( Long.parseLong( val ) * mult );
	}
	
	public static String
	encodeJVMLong(
		long	val )
	{
		if ( val < 1024 ){
			
			return( String.valueOf( val ));
		}
		
		val = val/1024;
		
		if ( val < 1024 ){
			
			return( String.valueOf( val ) + "k" );
		}
		
		val = val/1024;
		
		if ( val < 1024 ){
			
			return( String.valueOf( val ) + "m" );
		}
		
		val = val/1024;
		
		return( String.valueOf( val ) + "g" );
	}
}
