/*
 * Created on 1 Nov 2006
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, 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; 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.
 *
 */


package com.aelitis.azureus.core.stats;

import java.util.*;
import java.util.regex.Pattern;

import org.gudy.azureus2.core3.util.AEDiagnostics;
import org.gudy.azureus2.core3.util.AEDiagnosticsEvidenceGenerator;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.IndentWriter;
import org.gudy.azureus2.core3.util.Timer;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;

import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.core.util.average.Average;
import com.aelitis.azureus.core.util.average.AverageFactory;

public class 
AzureusCoreStats 
{
	public static final int AVERAGE_PERIOD	= 1000;
	
	public static final String ST_ALL							= ".*";
	
		// DISK
	
	public static final String ST_DISK							= "disk.*";
	public static final String ST_DISK_READ_QUEUE_LENGTH		= "disk.read.queue.length";	
	public static final String ST_DISK_READ_QUEUE_BYTES			= "disk.read.queue.bytes";	
	public static final String ST_DISK_READ_REQUEST_COUNT		= "disk.read.request.count";	
	public static final String ST_DISK_READ_REQUEST_SINGLE		= "disk.read.request.single";	
	public static final String ST_DISK_READ_REQUEST_MULTIPLE	= "disk.read.request.multiple";	
	public static final String ST_DISK_READ_REQUEST_BLOCKS		= "disk.read.request.blocks";	
	public static final String ST_DISK_READ_BYTES_TOTAL			= "disk.read.bytes.total";	
	public static final String ST_DISK_READ_BYTES_SINGLE		= "disk.read.bytes.single";	
	public static final String ST_DISK_READ_BYTES_MULTIPLE		= "disk.read.bytes.multiple";	
	public static final String ST_DISK_READ_IO_TIME				= "disk.read.io.time";	
	public static final String ST_DISK_READ_IO_COUNT			= "disk.read.io.count";	
	
	public static final String ST_DISK_WRITE_QUEUE_LENGTH		= "disk.write.queue.length";	
	public static final String ST_DISK_WRITE_QUEUE_BYTES		= "disk.write.queue.bytes";	
	public static final String ST_DISK_WRITE_REQUEST_COUNT		= "disk.write.request.count";	
	public static final String ST_DISK_WRITE_REQUEST_BLOCKS		= "disk.write.request.blocks";	
	public static final String ST_DISK_WRITE_BYTES_TOTAL		= "disk.write.bytes.total";	
	public static final String ST_DISK_WRITE_BYTES_SINGLE		= "disk.write.bytes.single";	
	public static final String ST_DISK_WRITE_BYTES_MULTIPLE		= "disk.write.bytes.multiple";	
	public static final String ST_DISK_WRITE_IO_TIME			= "disk.write.io.time";	
	public static final String ST_DISK_WRITE_IO_COUNT			= "disk.write.io.count";	
	
		// NETWORK
	
	public static final String ST_NET_WRITE_CONTROL_WAIT_COUNT			= "net.write.control.wait.count";	
	public static final String ST_NET_WRITE_CONTROL_NP_COUNT			= "net.write.control.np.count";	
	public static final String ST_NET_WRITE_CONTROL_P_COUNT				= "net.write.control.p.count";		
	public static final String ST_NET_WRITE_CONTROL_ENTITY_COUNT		= "net.write.control.entity.count";	
	public static final String ST_NET_WRITE_CONTROL_CON_COUNT			= "net.write.control.con.count";	
	public static final String ST_NET_WRITE_CONTROL_READY_CON_COUNT		= "net.write.control.ready.con.count";	
	public static final String ST_NET_WRITE_CONTROL_READY_BYTE_COUNT	= "net.write.control.ready.byte.count";	
	    
	public static final String ST_NET_READ_CONTROL_LOOP_COUNT			= "net.read.control.loop.count";	
	public static final String ST_NET_READ_CONTROL_NP_COUNT				= "net.read.control.np.count";	
	public static final String ST_NET_READ_CONTROL_P_COUNT				= "net.read.control.p.count";	
	public static final String ST_NET_READ_CONTROL_WAIT_COUNT			= "net.read.control.wait.count";	
	public static final String ST_NET_READ_CONTROL_ENTITY_COUNT			= "net.read.control.entity.count";	
	public static final String ST_NET_READ_CONTROL_CON_COUNT			= "net.read.control.con.count";	
	public static final String ST_NET_READ_CONTROL_READY_CON_COUNT		= "net.read.control.ready.con.count";	
	
		// TCP
	
	public static final String ST_NET_TCP_OUT_CONNECT_QUEUE_LENGTH		= "net.tcp.outbound.connect.queue.length";	
	public static final String ST_NET_TCP_OUT_PENDING_QUEUE_LENGTH		= "net.tcp.outbound.pending.queue.length";	
	public static final String ST_NET_TCP_OUT_CANCEL_QUEUE_LENGTH		= "net.tcp.outbound.cancel.queue.length";	
	public static final String ST_NET_TCP_OUT_CLOSE_QUEUE_LENGTH		= "net.tcp.outbound.close.queue.length";	

	public static final String ST_NET_TCP_SELECT_WRITE_COUNT			= "net.tcp.select.write.count";
	public static final String ST_NET_TCP_SELECT_READ_COUNT				= "net.tcp.select.read.count";

		// HTTP
	
	public static final String ST_NET_HTTP_IN_REQUEST_COUNT				= "net.http.inbound.request.count";			
	public static final String ST_NET_HTTP_IN_REQUEST_OK_COUNT			= "net.http.inbound.request.ok.count";		
	public static final String ST_NET_HTTP_IN_REQUEST_INVALID_COUNT		= "net.http.inbound.request.invalid.count";		
	public static final String ST_NET_HTTP_IN_REQUEST_WEBSEED_COUNT		= "net.http.inbound.request.webseed.count";	
	public static final String ST_NET_HTTP_IN_REQUEST_GETRIGHT_COUNT	= "net.http.inbound.request.getright.count";	
	
		// Peer Control
	
	
	public static final String ST_PEER_CONTROL_SCHEDULE_COUNT	= "peer.control.schedule.count";
	public static final String ST_PEER_CONTROL_LOOP_COUNT		= "peer.control.loop.count";
	public static final String ST_PEER_CONTROL_YIELD_COUNT		= "peer.control.yield.count";
	public static final String ST_PEER_CONTROL_WAIT_COUNT		= "peer.control.wait.count";
	public static final String ST_PEER_CONTROL_WAIT_TIME		= "peer.control.wait.time";

		// Peer Manager
	
	public static final String ST_PEER_MANAGER_COUNT					= "peer.manager.count";
	public static final String ST_PEER_MANAGER_PEER_COUNT				= "peer.manager.peer.count";
	public static final String ST_PEER_MANAGER_PEER_SNUBBED_COUNT		= "peer.manager.peer.snubbed.count";
	public static final String ST_PEER_MANAGER_PEER_STALLED_DISK_COUNT	= "peer.manager.peer.stalled.disk.count";

		// Tracker
	
	public static final String ST_TRACKER_READ_BYTES		= "tracker.read.bytes.total";
	public static final String ST_TRACKER_WRITE_BYTES		= "tracker.write.bytes.total";
	public static final String ST_TRACKER_ANNOUNCE_COUNT	= "tracker.announce.count";
	public static final String ST_TRACKER_ANNOUNCE_TIME		= "tracker.announce.time";
	public static final String ST_TRACKER_SCRAPE_COUNT		= "tracker.scrape.count";
	public static final String ST_TRACKER_SCRAPE_TIME		= "tracker.scrape.time";

		// xfer (persistent)
	
	public static final String ST_XFER_UPLOADED_PROTOCOL_BYTES		= "xfer.upload.protocol.bytes.total";
	public static final String ST_XFER_UPLOADED_DATA_BYTES			= "xfer.upload.data.bytes.total";
	public static final String ST_XFER_DOWNLOADED_PROTOCOL_BYTES	= "xfer.download.protocol.bytes.total";
	public static final String ST_XFER_DOWNLOADED_DATA_BYTES		= "xfer.download.data.bytes.total";

	
	public static final String	POINT 		= "Point";
	public static final String	CUMULATIVE 	= "Cumulative";
	
	private static final List		stats_names	= new ArrayList();
	private static final Map		stats_types	= new HashMap();
	
	private static final String[][] _ST_ALL = {
		
		{ ST_DISK_READ_QUEUE_LENGTH,				POINT },
		{ ST_DISK_READ_QUEUE_BYTES,					POINT },
		{ ST_DISK_READ_REQUEST_COUNT,				CUMULATIVE },
		{ ST_DISK_READ_REQUEST_SINGLE,				CUMULATIVE },
		{ ST_DISK_READ_REQUEST_MULTIPLE,			CUMULATIVE },
		{ ST_DISK_READ_REQUEST_BLOCKS,				CUMULATIVE },
		{ ST_DISK_READ_BYTES_TOTAL,					CUMULATIVE },
		{ ST_DISK_READ_BYTES_SINGLE,				CUMULATIVE },
		{ ST_DISK_READ_BYTES_MULTIPLE,				CUMULATIVE },
		{ ST_DISK_READ_IO_TIME,						CUMULATIVE },
		{ ST_DISK_READ_IO_COUNT,					CUMULATIVE },
				
		{ ST_DISK_WRITE_QUEUE_LENGTH,				POINT },
		{ ST_DISK_WRITE_QUEUE_BYTES,				POINT },
		{ ST_DISK_WRITE_REQUEST_COUNT,				CUMULATIVE },
		{ ST_DISK_WRITE_REQUEST_BLOCKS,				CUMULATIVE },
		{ ST_DISK_WRITE_BYTES_TOTAL,				CUMULATIVE },
		{ ST_DISK_WRITE_BYTES_SINGLE,				CUMULATIVE },
		{ ST_DISK_WRITE_BYTES_MULTIPLE,				CUMULATIVE },
		{ ST_DISK_WRITE_IO_TIME,					CUMULATIVE },
		{ ST_DISK_WRITE_IO_COUNT,					CUMULATIVE },

		{ ST_NET_WRITE_CONTROL_WAIT_COUNT,			CUMULATIVE },
		{ ST_NET_WRITE_CONTROL_P_COUNT,				CUMULATIVE },
		{ ST_NET_WRITE_CONTROL_NP_COUNT,			CUMULATIVE },
		{ ST_NET_WRITE_CONTROL_ENTITY_COUNT,		POINT },
		{ ST_NET_WRITE_CONTROL_CON_COUNT,			POINT },
		{ ST_NET_WRITE_CONTROL_READY_CON_COUNT,		POINT },
		{ ST_NET_WRITE_CONTROL_READY_BYTE_COUNT,	POINT },

		{ ST_NET_READ_CONTROL_LOOP_COUNT,			CUMULATIVE },
		{ ST_NET_READ_CONTROL_P_COUNT,				CUMULATIVE },
		{ ST_NET_READ_CONTROL_NP_COUNT,				CUMULATIVE },
		{ ST_NET_READ_CONTROL_WAIT_COUNT,			CUMULATIVE },
		{ ST_NET_READ_CONTROL_ENTITY_COUNT,			POINT },
		{ ST_NET_READ_CONTROL_CON_COUNT,			POINT },
		{ ST_NET_READ_CONTROL_READY_CON_COUNT,		POINT },
		
		{ ST_NET_TCP_OUT_CONNECT_QUEUE_LENGTH,		POINT },
		{ ST_NET_TCP_OUT_PENDING_QUEUE_LENGTH,		POINT },
		{ ST_NET_TCP_OUT_CANCEL_QUEUE_LENGTH,		POINT },
		{ ST_NET_TCP_OUT_CLOSE_QUEUE_LENGTH,		POINT },
		
		{ ST_NET_TCP_SELECT_WRITE_COUNT,			CUMULATIVE },
		{ ST_NET_TCP_SELECT_READ_COUNT,				CUMULATIVE },
		
		{ ST_NET_HTTP_IN_REQUEST_COUNT,				CUMULATIVE },
		{ ST_NET_HTTP_IN_REQUEST_OK_COUNT,			CUMULATIVE },
		{ ST_NET_HTTP_IN_REQUEST_INVALID_COUNT,		CUMULATIVE },
		{ ST_NET_HTTP_IN_REQUEST_WEBSEED_COUNT,		CUMULATIVE },
		{ ST_NET_HTTP_IN_REQUEST_GETRIGHT_COUNT,	CUMULATIVE },
		
		{ ST_PEER_CONTROL_SCHEDULE_COUNT,			CUMULATIVE },
		{ ST_PEER_CONTROL_LOOP_COUNT,				CUMULATIVE },
		{ ST_PEER_CONTROL_YIELD_COUNT,				CUMULATIVE },
		{ ST_PEER_CONTROL_WAIT_COUNT,				CUMULATIVE },
		{ ST_PEER_CONTROL_WAIT_TIME,				CUMULATIVE },
		
		{ ST_PEER_MANAGER_COUNT,					POINT },
		{ ST_PEER_MANAGER_PEER_COUNT,				POINT },
		{ ST_PEER_MANAGER_PEER_SNUBBED_COUNT,		POINT },
		{ ST_PEER_MANAGER_PEER_STALLED_DISK_COUNT,	POINT },
		
		{ ST_TRACKER_READ_BYTES,					CUMULATIVE },
		{ ST_TRACKER_WRITE_BYTES,					CUMULATIVE },
		{ ST_TRACKER_ANNOUNCE_COUNT,				CUMULATIVE },
		{ ST_TRACKER_ANNOUNCE_TIME,					CUMULATIVE },
		{ ST_TRACKER_SCRAPE_COUNT,					CUMULATIVE },
		{ ST_TRACKER_SCRAPE_TIME,					CUMULATIVE },

		{ ST_XFER_UPLOADED_PROTOCOL_BYTES,			CUMULATIVE },
		{ ST_XFER_UPLOADED_DATA_BYTES,				CUMULATIVE },
		{ ST_XFER_DOWNLOADED_PROTOCOL_BYTES,		CUMULATIVE },
		{ ST_XFER_DOWNLOADED_DATA_BYTES,			CUMULATIVE },	
	};
	
	static{
		
		addStatsDefinitions( _ST_ALL );
		
		AEDiagnostics.addEvidenceGenerator(
			new AEDiagnosticsEvidenceGenerator()
			{
		  		public void 
	    		generate(
	    			IndentWriter writer ) 
	    		{
					writer.println( "Stats" );
						
					boolean	turn_on_averages = !getEnableAverages();

					try{
						writer.indent();	
						
						if ( turn_on_averages ){
							
							setEnableAverages( true );
							
							try{
								Thread.sleep( AVERAGE_PERIOD * 5 );
								
							}catch( Throwable e ){
							}
						}
												
						Set	types = new HashSet();
						
						types.add( ST_ALL );
						
						Map	reply = getStats( types );
						
						Iterator	it = reply.entrySet().iterator();
						
						List	lines = new ArrayList();
						
						while( it.hasNext()){
							
							Map.Entry	entry = (Map.Entry)it.next();
							
							lines.add( entry.getKey() + " -> " + entry.getValue());
						}
						
						Collections.sort( lines );
				
						for ( int i=0;i<lines.size();i++){

							writer.println((String)lines.get(i));
						}
						
					}finally{
						
						if ( turn_on_averages ){
							
							setEnableAverages( false );
						}
						
						writer.exdent();
					}
	    		}
			});
	}
	
	private static final CopyOnWriteList	providers 	= new CopyOnWriteList();
	
	private static  Map	averages	= new HashMap();
	
	private static boolean 	enable_averages;
	private static Timer	average_timer;
	
	private static CopyOnWriteList provider_listeners = new CopyOnWriteList();
	private static CopyOnWriteList derived_generators = new CopyOnWriteList();
	
	public static void
	addStatsDefinitions(
		String[][]		stats )
	{
		for (int i=0;i<stats.length;i++){
			
			String	name = stats[i][0];
			
			stats_names.add( name );
			
			stats_types.put( name, stats[i][1] );
		}
	}
	
	public static Map
	getStats(
		Set		types )
	{
		Set	expanded = new HashSet();
		
		Iterator	it = types.iterator();
		
		while( it.hasNext()){
			
			String	type = (String)it.next();
			
			if ( type.endsWith( ".average" )){
				
				type = type.substring(0,type.length()-8);
			}
			
			if ( !type.endsWith("*")){
				
				type = type + ".*";
			}
			
			Pattern pattern = Pattern.compile( type );
						
			for (int i=0;i<stats_names.size();i++){
				
				String	s = (String)stats_names.get(i);
				
				if ( pattern.matcher( s ).matches()){
					
					expanded.add( s );
				}
			}
			
			Iterator provider_it = providers.iterator();
			
			while( provider_it.hasNext()){
				
				Object[]	provider_entry = (Object[])provider_it.next();
				
				Set provider_types = (Set)provider_entry[0];
				
				Iterator pt_it = provider_types.iterator();
				
				while( pt_it.hasNext()){
					
					String	s = (String)pt_it.next();
					
					if ( pattern.matcher( s ).matches()){
						
						expanded.add( s );
					}
				}
			}
			
			Iterator derived_it = derived_generators.iterator();
			
			while( derived_it.hasNext()){
				
				try{
					
					((derivedStatsGenerator)derived_it.next()).match( pattern, expanded );
					
				}catch( Throwable e ){
					
					Debug.printStackTrace( e );
				}
			}
		}
		
		Map	result = getStatsSupport( expanded );
		
		Map	ave = averages;
		
		if ( ave != null ){
			
			it = result.keySet().iterator();
			
			Map	ave_results = new HashMap();
			
			while( it.hasNext()){
								
				String	key = (String)it.next();
				
				Object[]	a_entry = (Object[])ave.get( key );
				
				if ( a_entry != null ){
					
					Average	average = (Average)a_entry[0];
					
					ave_results.put( key + ".average", new Long((long)average.getAverage()));
				}
			}
						
			result.putAll( ave_results );
		}
		
		Iterator derived_it = derived_generators.iterator();
				
		while( derived_it.hasNext()){
			
			try{
				
				((derivedStatsGenerator)derived_it.next()).generate( result );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace( e );
			}
		}
		
		return( result );
	}
	
	protected static Map
	getStatsSupport(
		Set		types )
	{
		Map	result = new HashMap();
		
		Iterator it = providers.iterator();
		
		while( it.hasNext()){
			
			Object[]	provider_entry = (Object[])it.next();
			
			Map	provider_result = new HashMap();

			Set	target_types;
			
			if ( types == null ){
				
				target_types = (Set)provider_entry[0];
			}else{
			
				target_types = types;
			}
			
			try{
				((AzureusCoreStatsProvider)provider_entry[1]).updateStats( target_types, provider_result );
				
				Iterator pit = provider_result.entrySet().iterator();
				
				while( pit.hasNext()){
					
					Map.Entry	pe = (Map.Entry)pit.next();
					
					String	key = (String)pe.getKey();
					Object	obj	= pe.getValue();
					
					if ( obj instanceof Long ){
						
						Long	old = (Long)result.get(key);
						
						if ( old == null ){
							
							result.put( key, obj );
							
						}else{
							
							long	v = ((Long)obj).longValue();
							
							result.put( key, new Long( v + old.longValue()));
						}
					}else{
						
						result.put( key, obj );
					}
				}
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
		
		return( result );
	}
	
	public static void
	registerProvider(
		Set							types,
		AzureusCoreStatsProvider	provider )
	{
		synchronized( providers ){
			
			providers.add( new Object[]{ types, provider });
		}
		
		fireProvidersChangeListeners();
	}

	public static void
	addProvidersChangeListener(
		providersChangeListener		l )
	{
		provider_listeners.add( l );
	}

	protected static void
	fireProvidersChangeListeners()
	{
		Iterator it = provider_listeners.iterator();
		
		while( it.hasNext()){
			
			try{
				((providersChangeListener)it.next()).providersChanged();
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}
	
	public static void
	registerDerivedStatsGenerator(
		derivedStatsGenerator	gen )
	{
		derived_generators.add( gen );
	}
	
	public static synchronized void
	setEnableAverages(
		boolean		enabled )
	{
		if ( enabled == enable_averages ){
			
			return;
		}
		
		enable_averages = enabled;
		
		if ( enabled ){
			
			if ( average_timer == null ){
				
				average_timer = new Timer( "AzureusCoreStats:average" );
				
				averages = new HashMap();
				
				average_timer.addPeriodicEvent(
					AVERAGE_PERIOD,
					new TimerEventPerformer()
					{
						private Map	ave = averages;

						public void
						perform(
							TimerEvent	event )
						{
							Map	stats = getStatsSupport( null );
																
							Iterator	it = stats.entrySet().iterator();
							
							boolean	new_averages = false;
							
							while( it.hasNext()){
								
								Map.Entry	entry = (Map.Entry)it.next();
								
								String	key 	= (String)entry.getKey();
								Object	value 	= entry.getValue();
								
								if ( value instanceof Long ){
									
									long	last_value;
									Average	a;
									boolean	new_average;
									
									Object[] a_entry = (Object[])ave.get( key );
									
									if ( a_entry == null ){
	
										a 			= AverageFactory.MovingImmediateAverage( 10 );
										last_value	= 0;
										
										a_entry = new Object[]{ a, value };
										
										ave.put( key, a_entry );
										
										new_averages = new_average = true;
										
									}else{
										a			= (Average)a_entry[0];
										last_value	= ((Long)a_entry[1]).longValue();
										
										new_average = false;
									}
									
									if ( stats_types.get( key ) == CUMULATIVE ){
									
											// skip initial value as 'last_value' is invalid
										
										if ( !new_average ){
										
											a.update(((Long)value).longValue() - last_value);
										}
									}else{
										
										a.update(((Long)value).longValue());

									}
									
									a_entry[1] = value;
								}
							}
							
							if ( new_averages ){
								
								fireProvidersChangeListeners();
							}
						}
					});
			}
		}else{
			
			if ( average_timer != null ){
				
				average_timer.destroy();
				
				average_timer = null;
				
				averages	= null;
			}
		}
	}
	
	public static synchronized boolean
	getEnableAverages()
	{
		return( enable_averages );
	}
	
	public interface
	providersChangeListener
	{
		public void
		providersChanged();
	}
	
	public interface
	derivedStatsGenerator
	{
		public void
		match(
			Pattern		p,
			Set			required );
		
		public void
		generate(
			Map	map );
	}
}
