/*
 * Created on Apr 16, 2004 Created by Alon Rohter Copyright (C) 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.util;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Utility class to retrieve current system time, and catch clock backward time
 * changes.
 */
public class SystemTime {
	public static final long			TIME_GRANULARITY_MILLIS	= 25;	//internal update time ms
	private static SystemTimeProvider	instance;
	
	// can't do that without some safeguarding code.
	// monotime does guarantee that time neither goes backwards nor performs leaps into the future.
	// the HPC doesn't jump backward but can jump forward in time
	private static final boolean		SOD_IT_LETS_USE_HPC = false;//	= Constants.isCVSVersion();
	
	private static volatile List		systemTimeConsumers		= new ArrayList();
	private static volatile List		monotoneTimeConsumers	= new ArrayList();
	private static volatile List		clock_change_list		= new ArrayList();
	private static long					hpc_base_time;
	private static long					hpc_last_time;
	private static boolean				no_hcp_logged;

	static
	{
		try
		{
			if (System.getProperty("azureus.time.use.raw.provider", "0").equals("1"))
			{
				System.out.println("Warning: Using Raw Provider");
				instance = new RawProvider();
			} else
			{
				instance = new SteppedProvider();
			}
		} catch (Throwable e)
		{
			// might be in applet...
			instance = new SteppedProvider();
		}
	}

	public static void useRawProvider() {
		if (!(instance instanceof RawProvider))
		{
			Debug.out( "Whoa, someone already created a non-raw provider!" );
			
			instance = new RawProvider();
		}
	}

	protected interface SystemTimeProvider {
		public long getTime();

		public long getMonoTime();
		
		public long
		getSteppedMonoTime();
	}

	protected static class SteppedProvider implements SystemTimeProvider {
		private static final int	STEPS_PER_SECOND	= (int) (1000 / TIME_GRANULARITY_MILLIS);
		private static final long	HPC_START = getHighPrecisionCounter()/1000000L;
		
		private final Thread		updater;
		private volatile long		stepped_time;
		private volatile long		currentTimeOffset = System.currentTimeMillis();
		private AtomicLong 			last_approximate_time = new AtomicLong(); 
		//private volatile long		last_approximate_time;
		private volatile int		access_count;
		private volatile int		slice_access_count;
		private volatile int		access_average_per_slice;
		private volatile int		drift_adjusted_granularity;
		
		private volatile long		stepped_mono_time;

		private SteppedProvider()
		{
			// System.out.println("SystemTime: using stepped time provider");
			
			stepped_time = 0;
			
			updater = new Thread("SystemTime")
			{
				public void run() {
					long adjustedTimeOffset = currentTimeOffset;
					// these averages rely on monotone time, thus won't be affected by system time changes
					final Average access_average = Average.getInstance(1000, 10);
					final Average drift_average = Average.getInstance(1000, 10);
					long lastOffset = adjustedTimeOffset;
					long lastSecond = -1000;
					int tick_count = 0;
					while (true)
					{
						long rawTime = System.currentTimeMillis();
						/*
						 * keep the monotone time in sync with the raw system
						 * time, for this we need to know the offset of the
						 * current time to the system time
						 */
						long newMonotoneTime = rawTime - adjustedTimeOffset;
						long delta = newMonotoneTime - stepped_time;
						/*
						 * unless the system time jumps, then we just guess the
						 * time that has passed and adjust the update, so that
						 * the next round can be in sync with the system time
						 * again
						 */
						if (delta < 0 || delta > 1000)
						{
							/*
							 * jump occured, update monotone time offset, but
							 * not the current time one, that only happens every
							 * second
							 */ 
							stepped_time += TIME_GRANULARITY_MILLIS;
							adjustedTimeOffset = rawTime - stepped_time;
						} else
						{ // time is good, keep it
							stepped_time = newMonotoneTime;
						}
						tick_count++;
						if (tick_count == STEPS_PER_SECOND)
						{
							if (lastOffset != adjustedTimeOffset)
							{
								final long change = adjustedTimeOffset - lastOffset;
								Iterator it = clock_change_list.iterator();
								//Debug.outNoStack("Clock change of " + change + "ms detected");
								while (it.hasNext())
								{
									((ChangeListener) it.next()).clockChanged(rawTime, change);
								}
								lastOffset = adjustedTimeOffset;
								// make the internal offset publicly visible after consumers have been notified
								currentTimeOffset = adjustedTimeOffset;
							}
							// averaging magic to estimate the amount of time that passes between each getTime invocation
							long drift = stepped_time - lastSecond - 1000;
							lastSecond = stepped_time;
							drift_average.addValue(drift);
							drift_adjusted_granularity = (int) (TIME_GRANULARITY_MILLIS + (drift_average.getAverage() / STEPS_PER_SECOND));
							access_average.addValue(access_count);
							access_average_per_slice = (int) (access_average.getAverage() / STEPS_PER_SECOND);
							//System.out.println( "access count = " + access_count + ", average = " + access_average.getAverage() + ", per slice = " + access_average_per_slice + ", drift = " + drift +", average = " + drift_average.getAverage() + ", dag =" + drift_adjusted_granularity );
							access_count = 0;
							tick_count = 0;
						}
						slice_access_count = 0;
						
						stepped_mono_time = stepped_time;
						
						// copy reference since we use unsynced COW semantics
						List consumersRef = monotoneTimeConsumers;
						for (int i = 0; i < consumersRef.size(); i++)
						{
							TickConsumer cons = (TickConsumer) consumersRef.get(i);
							try
							{
								cons.consume(stepped_time);
							} catch (Throwable e)
							{
								Debug.printStackTrace(e);
							}
						}
						
						/*
						 * notify consumers with the external offset, internal
						 * offset is only meant for updates
						 */
						consumersRef = systemTimeConsumers;
						long adjustedTime = stepped_time + currentTimeOffset;
						for (int i = 0; i < consumersRef.size(); i++)
						{
							TickConsumer cons = (TickConsumer) consumersRef.get(i);
							try
							{
								cons.consume(adjustedTime);
							} catch (Throwable e)
							{
								Debug.printStackTrace(e);
							}
						}
						
						try
						{
							Thread.sleep(TIME_GRANULARITY_MILLIS);
						} catch (Exception e)
						{
							Debug.printStackTrace(e);
						}
					}
				}
			};
			updater.setDaemon(true);
			// we don't want this thread to lag much as it'll stuff up the upload/download rate mechanisms (for example)
			updater.setPriority(Thread.MAX_PRIORITY);
			updater.start();
		}

		public long getTime() {
			return getMonoTime() + currentTimeOffset;
		}

		public long getMonoTime() {
			if ( SOD_IT_LETS_USE_HPC ){
				
				return( ( getHighPrecisionCounter()/1000000) - HPC_START );
				
			}else{
				long adjusted_time;
				long averageSliceStep = access_average_per_slice;
				if (averageSliceStep > 0)
				{
					long sliceStep = (drift_adjusted_granularity * slice_access_count) / averageSliceStep;
					if (sliceStep >= drift_adjusted_granularity)
					{
						sliceStep = drift_adjusted_granularity - 1;
					}
					adjusted_time = sliceStep + stepped_time;
				} else
					adjusted_time = stepped_time;
				access_count++;
				slice_access_count++;
	
				// make sure we don't go backwards and our reference value for going backwards doesn't go backwards either
				long approxBuffered = last_approximate_time.get(); 
				if (adjusted_time < approxBuffered)
					adjusted_time = approxBuffered;				
				else
					last_approximate_time.compareAndSet(approxBuffered, adjusted_time);
	
				return adjusted_time;
			}
		}
		
		public long getSteppedMonoTime() {

			if ( SOD_IT_LETS_USE_HPC ){
				
				return( getHighPrecisionCounter()/1000000 );
				
			}else{
				
				return( stepped_mono_time );
			}
		}
	}
	
	protected static class RawProvider implements SystemTimeProvider {
		private static final int	STEPS_PER_SECOND	= (int) (1000 / TIME_GRANULARITY_MILLIS);
		private final Thread		updater;

		private RawProvider()
		{
			System.out.println("SystemTime: using raw time provider");
			
			updater = new Thread("SystemTime")
			{
				long	last_time;

				public void run() {
					while (true)
					{
						long current_time = getTime();
						if (last_time != 0)
						{
							long offset = current_time - last_time;
							if (offset < 0 || offset > 5000)
							{
								// clock's changed
								Iterator it = clock_change_list.iterator();
								while (it.hasNext())
								{									
									((ChangeListener) it.next()).clockChanged(current_time, offset);
								}
							}
						}
						last_time = current_time;
						List consumer_list_ref = systemTimeConsumers;
						for (int i = 0; i < consumer_list_ref.size(); i++)
						{
							TickConsumer cons = (TickConsumer) consumer_list_ref.get(i);
							try
							{
								cons.consume(current_time);
							} catch (Throwable e)
							{
								Debug.printStackTrace(e);
							}
						}
						consumer_list_ref = monotoneTimeConsumers;
						
						long	mono_time = getMonoTime();
						
						for (int i = 0; i < consumer_list_ref.size(); i++)
						{
							TickConsumer cons = (TickConsumer) consumer_list_ref.get(i);
							try
							{
								cons.consume(mono_time);
							} catch (Throwable e)
							{
								Debug.printStackTrace(e);
							}
						}
						
						try
						{
							Thread.sleep(TIME_GRANULARITY_MILLIS);
						} catch (Exception e)
						{
							Debug.printStackTrace(e);
						}
					}
				}
			};
			updater.setDaemon(true);
			// we don't want this thread to lag much as it'll stuff up the upload/download rate mechanisms (for example)
			updater.setPriority(Thread.MAX_PRIORITY);
			updater.start();
		}

		public long getTime() {
			return System.currentTimeMillis();
		}

		/**
		 * This implementation does not guarantee monotonous time increases with
		 * 100% accuracy as the adjustedTimeOffset is only adjusted every
		 * TIME_GRANULARITY_MILLIS
		 */
		public long getMonoTime() {
			return getHighPrecisionCounter()/1000000;
		}
		
		public long getSteppedMonoTime() {
			return getMonoTime();
		}
	}

	/**
	 * Note that this can this time can jump into the future or past due to
	 * clock adjustments use getMonotonousTime() if you need steady increases
	 * 
	 * @return current system time in millisecond since epoch
	 */
	public static long getCurrentTime() {
		return (instance.getTime());
	}

	/**
	 * Time that is guaranteed to grow monotonously and also ignores larger
	 * jumps into the future which might be caused by adjusting the system clock<br>
	 * <br>
	 * 
	 * <b>Do not mix times retrieved by this method with normal time!</b>
	 * 
	 * @return the amount of real time passed since the program start in
	 *         milliseconds
	 */
	public static long getMonotonousTime() {
		return instance.getMonoTime();
	}

		/**
		 * Like getMonotonousTime but only updated at TIME_GRANULARITY_MILLIS intervals (not interpolated)
		 * As such it is likely to be cheaper to obtain
		 * @return
		 */
	
	public static long getSteppedMonotonousTime() {
		return instance.getSteppedMonoTime();
	}

	
	public static long getOffsetTime(long offsetMS) {
		return instance.getTime() + offsetMS;
	}

	public static void registerConsumer(TickConsumer c) {
		synchronized (instance)
		{
			List new_list = new ArrayList(systemTimeConsumers);
			new_list.add(c);
			systemTimeConsumers = new_list;
		}
	}

	public static void unregisterConsumer(TickConsumer c) {
		synchronized (instance)
		{
			List new_list = new ArrayList(systemTimeConsumers);
			new_list.remove(c);
			systemTimeConsumers = new_list;
		}
	}

	public static void registerMonotonousConsumer(TickConsumer c) {
		synchronized (instance)
		{
			List new_list = new ArrayList(monotoneTimeConsumers);
			new_list.add(c);
			monotoneTimeConsumers = new_list;
		}
	}

	public static void unregisterMonotonousConsumer(TickConsumer c) {
		synchronized (instance)
		{
			List new_list = new ArrayList(monotoneTimeConsumers);
			new_list.remove(c);
			monotoneTimeConsumers = new_list;
		}
	}

	public static void registerClockChangeListener(ChangeListener c) {
		synchronized (instance)
		{
			List new_list = new ArrayList(clock_change_list);
			new_list.add(c);
			clock_change_list = new_list;
		}
	}

	public static void unregisterClockChangeListener(ChangeListener c) {
		synchronized (instance)
		{
			List new_list = new ArrayList(clock_change_list);
			new_list.remove(c);
			clock_change_list = new_list;
		}
	}

	public interface TickConsumer {
		public void consume(long current_time);
	}

	public interface ChangeListener {
		public void clockChanged(long current_time, long change_millis);
	}

	public static long 
	getHighPrecisionCounter() 
	{
		return( System.nanoTime());
	}

	public static void main(String[] args) {
		for (int i = 0; i < 1; i++)
		{
			//final int f_i = i;
			new Thread()
			{
				public void run() {
					/*
					 * Average access_average = Average.getInstance( 1000, 10 );
					 * 
					 * long last = SystemTime.getCurrentTime();
					 * 
					 * int count = 0;
					 * 
					 * while( true ){
					 * 
					 * long now = SystemTime.getCurrentTime();
					 * 
					 * long diff = now - last;
					 * 
					 * System.out.println( "diff=" + diff );
					 * 
					 * last = now;
					 * 
					 * access_average.addValue( diff );
					 * 
					 * count++;
					 * 
					 * if ( count == 33 ){
					 * 
					 * System.out.println( "AVERAGE " + f_i + " = " +
					 * access_average.getAverage());
					 * 
					 * count = 0; }
					 * 
					 * try{ Thread.sleep( 3 );
					 * 
					 * }catch( Throwable e ){ } }
					 */
					long cstart = SystemTime.getCurrentTime();
					long mstart = SystemTime.getMonotonousTime();
					System.out.println("alter system clock to see differences between monotonous and current time");
					long cLastRound = cstart;
					long mLastRound = mstart;
					while (true)
					{
						long mnow = SystemTime.getMonotonousTime();
						long cnow = SystemTime.getCurrentTime();
						//if(mLastRound > mnow)
						System.out.println("current: " + (cnow - cstart) + " monotonous:" + (mnow - mstart) + " delta current:" + (cnow - cLastRound) + " delta monotonous:" + (mnow - mLastRound));
						cLastRound = cnow;
						mLastRound = mnow;
						try
						{
							Thread.sleep(15);
						} catch (Throwable e)
						{}
					}
				}
			}.start();
		}
	}
}
