/*
 * Created on May 12, 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.download.impl;

import java.util.*;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.peer.PEPeerManager;
import org.gudy.azureus2.core3.peer.PEPeerManagerStats;
import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.AsyncDispatcher;
import org.gudy.azureus2.core3.util.DisplayFormatters;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;
import org.gudy.azureus2.core3.util.TimerEventPeriodic;

import com.aelitis.azureus.core.AzureusCore;
import com.aelitis.azureus.core.AzureusCoreFactory;
import com.aelitis.azureus.core.networkmanager.LimitedRateGroup;
import com.aelitis.azureus.core.networkmanager.NetworkManager;
import com.aelitis.azureus.core.speedmanager.SpeedManager;
import com.aelitis.azureus.core.speedmanager.SpeedManagerLimitEstimate;
import com.aelitis.azureus.core.speedmanager.SpeedManagerPingMapper;


public class 
DownloadManagerRateController 
{
	private static AzureusCore		core;
	private static SpeedManager		speed_manager;
	
	private static Map<PEPeerManager,PMState>		pm_map = new HashMap<PEPeerManager, PMState>();
	
	private static TimerEventPeriodic	timer;
	
	private static AsyncDispatcher	dispatcher = new AsyncDispatcher( "DMCRateController" );
	
	private static boolean	enable;
	private static boolean 	enable_limit_handling;
	private static int		slack_bytes_per_sec;
	
	static{
		COConfigurationManager.addAndFireParameterListeners(
			new String[]{
				"Bias Upload Enable",
				"Bias Upload Handle No Limit",
				"Bias Upload Slack KBs",
			},
			new ParameterListener()
			{
				public void 
				parameterChanged(
					String parameterName) 
				{
					enable				 	= COConfigurationManager.getBooleanParameter( "Bias Upload Enable" );
					enable_limit_handling 	= COConfigurationManager.getBooleanParameter( "Bias Upload Handle No Limit" ) && enable;
					slack_bytes_per_sec		= COConfigurationManager.getIntParameter( "Bias Upload Slack KBs" )*1024;
				}
			});
	}
	

	private static volatile int rate_limit	= 0;
	
	private static LimitedRateGroup 
		limiter = 
			new LimitedRateGroup()
			{
				public String 
				getName()
				{
					return( "DMRC" );
				}
		
				public int 
				getRateLimitBytesPerSecond()
				{
					return( rate_limit );
				}
			};
	
	private static final int TIMER_MILLIS			= 1000;
			
	private static final int WAIT_AFTER_CHOKE_PERIOD	= 10*1000;
	private static final int WAIT_AFTER_CHOKE_TICKS		= WAIT_AFTER_CHOKE_PERIOD/TIMER_MILLIS;

	private static final int DEFAULT_UP_LIMIT	= 250*1024;
	private static final int MAX_UP_DIFF	= 15*1024;
	private static final int MAX_DOWN_DIFF	= 10*1024;
	private static final int MIN_DIFF	= 2*1024;
				
	private static final int		SAMPLE_COUNT			= 5;
	private static int				sample_num;
	private static double			incomplete_samples;
	private static double			complete_samples;
	
	private static int	ticks_to_sample_start;
	
	private static int		last_rate_limit;
	private static double	last_incomplete_average;
	private static double	last_complete_average;
	private static double	last_overall_average;


	
	private static int	tick_count				= 0;
	private static int	last_tick_processed 	= -1;
	
	private static long pm_last_bad_limit;
	private static int	latest_choke;
	private static int	wait_until_tick;
	
	public static String
	getString()
	{
		if ( enable ){
			
			String	str = "reserved=" + DisplayFormatters.formatByteCountToKiBEtcPerSec( slack_bytes_per_sec );
			
			if ( enable_limit_handling ){
			
				str += ", limit=" + DisplayFormatters.formatByteCountToKiBEtcPerSec( rate_limit );
				
				str += 	", last[choke=" + DisplayFormatters.formatByteCountToKiBEtcPerSec( latest_choke ) + 
						", ratio=" + DisplayFormatters.formatDecimal(last_incomplete_average/last_complete_average, 2) + "]";
			
				return( str );
				
			}else{
				
				return( str );
			}
		}else{
			
			return( "Disabled" );
		}
	}
	
	public static void
	addPeerManager(
		final PEPeerManager		pm )
	{
		dispatcher.dispatch(
			new AERunnable()
			{
				public void
				runSupport()
				{
					if ( core == null ){
						
						core = AzureusCoreFactory.getSingleton();
						
						speed_manager = core.getSpeedManager();
					}
					
					boolean	is_complete = !pm.hasDownloadablePiece();
					
					PEPeerManagerStats pm_stats = pm.getStats();

					long	up_bytes = pm_stats.getTotalDataBytesSentNoLan() + pm_stats.getTotalProtocolBytesSentNoLan();

					if ( is_complete ){
						
						pm.addRateLimiter( limiter, true );
					}
					
					pm_map.put( pm, new PMState( pm, is_complete, up_bytes ));
										
					if ( timer == null ){
						
						timer = 
							SimpleTimer.addPeriodicEvent( 
								"DMRC", 
								TIMER_MILLIS,
								new TimerEventPerformer()
								{
									public void 
									perform(
										TimerEvent event ) 
									{
										dispatcher.dispatch(
											new AERunnable()
											{
												public void
												runSupport()
												{
													update();
												}
											});
									}
								});
					}
				}
			});
	}
	
	public static void
	removePeerManager(
		final PEPeerManager		pm )
	{
		dispatcher.dispatch(
			new AERunnable()
			{
				public void
				runSupport()
				{
					pm_map.remove( pm );
					
					if ( pm_map.size() == 0 ){
						
						timer.cancel();
						
						timer = null;
						
						rate_limit = 0;
					}
				}
			});
	}
	
	private static void
	update()
	{
		tick_count++;
		
		if ( 	(!enable_limit_handling ) ||  pm_map.size() == 0 ||  
				NetworkManager.isSeedingOnlyUploadRate() ||  NetworkManager.getMaxUploadRateBPSNormal() != 0 || 
				core == null || speed_manager == null || speed_manager.getSpeedTester() == null ){
			
			rate_limit = 0;
			
			return;
		}
						
		int	num_complete 	= 0;
		int	num_incomplete 	= 0;
		
		int	num_interesting = 0;
		
		int i_up_total	= 0;
		int c_up_total	= 0;
		
		long	mono_now = SystemTime.getMonotonousTime();
		
		for ( Map.Entry<PEPeerManager, PMState> entry: pm_map.entrySet()){
			
			PEPeerManager	pm 		= entry.getKey();
			PMState			state 	= entry.getValue();
			
			boolean	is_complete = !pm.hasDownloadablePiece();
			
			PEPeerManagerStats pm_stats = pm.getStats();
	
			long	up_bytes = pm_stats.getTotalDataBytesSentNoLan() + pm_stats.getTotalProtocolBytesSentNoLan();
						
			long	diff = state.setBytesUp( up_bytes );
			
			if ( is_complete ){
								
				num_complete++;
				
				c_up_total += diff;
				
			}else{
								
				num_incomplete++;
				
				i_up_total += diff;
				
				if ( state.isInteresting( mono_now )){
					
					num_interesting++;
				}
			}
				
			if ( state.isComplete() != is_complete ){
			
				if ( is_complete ){
					
					pm.addRateLimiter( limiter, true );
					
				}else{
					
					pm.removeRateLimiter( limiter, true );
				}
				
				state.setComplete( is_complete );
			}
		}
		
		if ( num_incomplete == 0 || num_complete == 0 || num_interesting == 0 ){
			
			rate_limit = 0;
			
			return;
		}
		
		boolean	skipped_tick = false;
		
		if ( last_tick_processed != tick_count - 1 ){
						
			pm_last_bad_limit 	= 0;
			latest_choke		= 0;
			wait_until_tick		= 0;

			ticks_to_sample_start	= 0;
			sample_num				= 0;
			incomplete_samples		= 0;
			complete_samples		= 0;
			
			skipped_tick = true;
		}
		
		last_tick_processed = tick_count;
		
		if ( skipped_tick || tick_count < wait_until_tick ){
			
			return;
		}
		
		try{
			long	real_now = SystemTime.getCurrentTime();
			
			SpeedManagerPingMapper mapper = speed_manager.getActiveMapper();
			
			if ( rate_limit == 0 ){
				
				rate_limit = speed_manager.getEstimatedUploadCapacityBytesPerSec().getBytesPerSec();
				
				if ( rate_limit == 0 ){
					
					rate_limit = DEFAULT_UP_LIMIT;
				}
			}
			
			SpeedManagerLimitEstimate last_bad = mapper.getLastBadUploadLimit();
						
			if ( last_bad != null ){
				
				int last_bad_limit = last_bad.getBytesPerSec();
								
				if ( last_bad_limit != pm_last_bad_limit ){
					
					pm_last_bad_limit = last_bad_limit;
			
					SpeedManagerLimitEstimate[] bad_ups = mapper.getBadUploadHistory();
		
					int		total 	= last_bad.getBytesPerSec();
					int		count	= 1;
					
					for ( SpeedManagerLimitEstimate bad: bad_ups ){
						
						long	t = bad.getWhen();
						
						if ( real_now - t <= 30*1000 && bad.getBytesPerSec() != last_bad_limit ){
							
							total += bad.getBytesPerSec();
							
							count++;
						}
					}
					
					latest_choke = total/count;
						
					int	new_rate_limit;
					
					if ( rate_limit == 0 ){
						
						new_rate_limit = latest_choke/2;
						
					}else{
						
						new_rate_limit = rate_limit/2;
					}
					
					if ( new_rate_limit < slack_bytes_per_sec ){
						
						new_rate_limit = slack_bytes_per_sec;
					}
					
					rate_limit = new_rate_limit;
					
					wait_until_tick = tick_count + WAIT_AFTER_CHOKE_TICKS;
					
					ticks_to_sample_start 	= 0;
					sample_num 				= 0;
					complete_samples		= 0;
					incomplete_samples		= 0;
					last_rate_limit			= 0;
					
					return;
				}
			}
			
			if ( ticks_to_sample_start > 0 ){
				
				ticks_to_sample_start--;
				
			}else if ( sample_num < SAMPLE_COUNT ){
				
				complete_samples 	+= c_up_total;
				incomplete_samples	+= i_up_total;
				
				sample_num++;
				
			}else{
					
				double	incomplete_average 	= incomplete_samples / SAMPLE_COUNT;
				double	complete_average 	= complete_samples / SAMPLE_COUNT;
				double	overall_average 	= ( complete_samples + incomplete_samples ) / SAMPLE_COUNT;
				
				int	action = -1;

				try{
					
					if ( last_rate_limit == 0 ){
						
						action = 1;
						
					}else{
						
						double overall_change = overall_average - last_overall_average;
						
						if ( overall_change < 0 ){
														
							if ( rate_limit < last_rate_limit ){
							
								// System.out.println( "average decreased" );

								action = 1;
								
							}else{
								
								action = 0;
							}
						}else{
							
							double last_ratio 	= last_incomplete_average / last_complete_average;
							double ratio		= incomplete_average / complete_average;
							
							// System.out.println( "rate=" + rate_limit + "/" + last_rate_limit + ", ratio=" + ratio + "/" + last_ratio );
							
							if ( rate_limit < last_rate_limit && ratio >= last_ratio ){
								
								action = -1;
								
							}else if ( rate_limit > last_rate_limit && ratio <= last_ratio ){
								
								double i_up_change = incomplete_average - last_incomplete_average;
								
								if ( i_up_change >= 1024 ){
								
									action = -1;
									
								}else{
									
									action = 1;
								}
								
							}else{
								
								action = 1;
							}
						}
					}
					
				}finally{
															
					int	new_rate_limit;

					if ( action > 0 ){
						
						int	ceiling = latest_choke==0?DEFAULT_UP_LIMIT:latest_choke;
						
						int	diff = ( ceiling - rate_limit )/4;

						if ( diff > MAX_UP_DIFF ){
							
							diff = MAX_UP_DIFF;
							
						}else if ( diff < MIN_DIFF ){
							
							diff = MIN_DIFF;
						}

						new_rate_limit = rate_limit + diff;
						
						if ( new_rate_limit > 100*1024*1024 ){
							
							new_rate_limit = 100*1024*1024;
						}
					}else if ( action < 0 ){
						
						int	diff = rate_limit/5;

						if ( diff > MAX_DOWN_DIFF ){
							
							diff = MAX_DOWN_DIFF;
							
						}else if ( diff < MIN_DIFF ){
							
							diff = MIN_DIFF;
						}

						new_rate_limit = rate_limit - diff;
						
						if ( new_rate_limit < slack_bytes_per_sec ){
							
							new_rate_limit = slack_bytes_per_sec;
						}
					}else{
						
						new_rate_limit = rate_limit;
					}
					
					last_rate_limit			= rate_limit;
					last_overall_average 	= overall_average;
					last_complete_average	= complete_average;
					last_incomplete_average	= incomplete_average;
					
					rate_limit = new_rate_limit;
					
					sample_num = 0;
					complete_samples	= 0;
					incomplete_samples	= 0;
				}
			}
	
		}finally{
			
			// System.out.println( "rate=" + DisplayFormatters.formatByteCountToKiBEtcPerSec( rate_limit ) + ", last_choke=" + latest_choke );
		}
	}
	
	private static class
	PMState
	{
		final private PEPeerManager	manager;
		
		private boolean			complete;
		private long			bytes_up;
		
		private boolean			interesting;
		private long			last_interesting_calc;
		
		private
		PMState(
			PEPeerManager	_manager,
			boolean			_complete,
			long			_bytes_up )
		{
			manager		= _manager;
			complete 	= _complete;
			bytes_up	= _bytes_up;
		}
		
		private boolean
		isComplete()
		{
			return( complete );
		}
		
		private void
		setComplete(
			boolean	c )
		{
			complete = c;
		}
		
		private long
		setBytesUp(
			long	b )
		{
			long diff = b - bytes_up;
			
			bytes_up = b;
			
			return( diff );
		}
		
		private boolean
		isInteresting(
			long		now )
		{
			boolean	calc;
					
			if ( last_interesting_calc == 0 ){
		
				calc = true;
				
			}else if ( !interesting ){
				
				calc = now - last_interesting_calc >= 5*1000;
				
			}else{
				
				calc = now - last_interesting_calc >= 60*1000;
			}
			
			if ( calc ){
				
				last_interesting_calc = now;
				
				PEPeerManagerStats stats = manager.getStats();
				
					// not interesting if stalled downloading
				
				long dl_rate = stats.getDataReceiveRate();
				
				if ( dl_rate < 5*1024 ){
					
					interesting = false;
					
				}else{
					
						// not interesting if we have nobody to seed to!
					
					if ( manager.getNbPeersUnchoked() < 3 ){
						
						interesting = false;
						
					}else{
						
							// see if the download has a manually imposed upload limit and if
							// we are close to it
						
						int limit = manager.getUploadRateLimitBytesPerSecond();
						
						if ( 	limit > 0 &&
								( stats.getDataSendRate() + stats.getProtocolSendRate() ) >= ( limit - (5*1024))){
							
							interesting = false;
							
						}else{
							
							interesting = true;
						}
					}
				}
			}
			
			return( interesting );
		}
	}
}
