/*
 * Created on 24-Apr-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.dht.netcoords;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.gudy.azureus2.core3.util.Debug;

import com.aelitis.azureus.core.dht.DHTStorageAdapter;
import com.aelitis.azureus.core.dht.impl.DHTLog;
import com.aelitis.azureus.core.util.CopyOnWriteList;

public class 
DHTNetworkPositionManager 
{
	private static DHTNetworkPositionProvider[]	providers = new DHTNetworkPositionProvider[0];
	private static Object providers_lock = new Object();
	
	private static DHTStorageAdapter	storage_adapter = null;
	
	private static CopyOnWriteList<DHTNetworkPositionProviderListener>		provider_listeners = new CopyOnWriteList<DHTNetworkPositionProviderListener>();
	private static volatile CopyOnWriteList<DHTNetworkPositionListener>		position_listeners;
	
	private static DHTNetworkPosition[] NP_EMPTY_ARRAY = {};
	
	public static void
	initialise(
		DHTStorageAdapter		adapter )
	{
		synchronized( providers_lock ){
			
			if ( storage_adapter == null ){
				
				storage_adapter	= adapter;
				
				for (int i=0;i<providers.length;i++){
				
					DHTNetworkPositionProvider	provider = providers[i];

					try{
						startUp( provider );
						
					}catch( Throwable e ){
						
						Debug.printStackTrace(e);
					}
				}
			}
		}
	}
	
	private static void
	startUp(
		DHTNetworkPositionProvider	provider )
	{
		byte[] data = null;
		
		if ( storage_adapter != null ){
			
			data = storage_adapter.getStorageForKey( "NPP:" + provider.getPositionType());
		}
		
		if ( data == null ){
			
			data = new byte[0];
		}
		
		try{
			provider.startUp( new DataInputStream( new ByteArrayInputStream( data )));
			
		}catch( Throwable e ){
			
			Debug.printStackTrace( e );
		}
	}
	
	private static void
	shutDown(
		DHTNetworkPositionProvider	provider )
	{
		try{			
			ByteArrayOutputStream	baos = new ByteArrayOutputStream();
			
			DataOutputStream	dos = new DataOutputStream( baos );
			
			provider.shutDown( dos );
			
			dos.flush();
			
			byte[]	data = baos.toByteArray();
			
			storage_adapter.setStorageForKey( "NPP:" + provider.getPositionType(), data );
			
		}catch( Throwable e ){
			
			Debug.printStackTrace( e );
		}
	}
	
	public static void
	destroy(
		DHTStorageAdapter		adapter )
	{
		synchronized( providers_lock ){
			
			if ( storage_adapter == adapter ){

				for (int i=0;i<providers.length;i++){

					shutDown( providers[i] );
				}
				
				storage_adapter	= null;
			}
		}
	}
	
	public static DHTNetworkPositionProviderInstance
	registerProvider(
		final DHTNetworkPositionProvider	provider )
	{
		boolean	fire_added = false;
		
		synchronized( providers_lock ){
	
			boolean						found 		= false;
			DHTNetworkPositionProvider	type_found	= null;
			
			for ( DHTNetworkPositionProvider p: providers ){
				
				if ( p == provider ){
					
					found = true;
					
					break;
					
				}else if ( p.getPositionType() == provider.getPositionType()){
					
					type_found = p;
				}
			}
			
			if ( !found ){
				
				if ( type_found != null ){
					
					Debug.out( "Registration of " + provider + " found previous provider for same position type, removing it" );
					
					unregisterProviderSupport( type_found );
				}
				
				DHTNetworkPositionProvider[]	new_providers = new DHTNetworkPositionProvider[providers.length + 1 ];
				
				System.arraycopy( providers, 0, new_providers, 0, providers.length );
				
				new_providers[providers.length] = provider;
				
				providers	= new_providers;
				
				if ( storage_adapter != null ){
				
					startUp( provider );
				}
				
				fire_added = true;
			}
		}
		
		if ( fire_added ){
			
			for ( DHTNetworkPositionProviderListener l: provider_listeners ){
				
				try{
					
					l.providerAdded( provider );
					
				}catch( Throwable e ){
					
					Debug.out( e );
				}
			}
		}
		
		return( new DHTNetworkPositionProviderInstance()
				{	
					public void
					log(
						String		log )
					{
						DHTLog.log("NetPos " + provider.getPositionType() + ": " + log );
					}
				});
	}
	
	public static void
	unregisterProvider(
		DHTNetworkPositionProvider	provider )
	{
		if ( unregisterProviderSupport( provider )){
			
			for ( DHTNetworkPositionProviderListener l: provider_listeners ){
				
				try{
					
					l.providerRemoved( provider );
					
				}catch( Throwable e ){
					
					Debug.out( e );
				}
			}
		}
	}
	
	private static boolean
	unregisterProviderSupport(
		DHTNetworkPositionProvider	provider )
	{
		boolean	removed = false;
		
		synchronized( providers_lock ){
	
			if ( providers.length == 0 ){
				
				return( false );
			}
			
			DHTNetworkPositionProvider[]	new_providers = new DHTNetworkPositionProvider[providers.length - 1 ];

			int	pos = 0;
			
			for ( int i=0;i<providers.length;i++){
				
				if ( providers[i] == provider ){
					
					if ( storage_adapter != null ){
						
						shutDown( provider );
					}
				}else{
					
					new_providers[pos++] = providers[i];
				}
			}
			if ( pos == new_providers.length ){
			
				providers = new_providers;
				
				removed = true;
			}
		}

		return( removed );
	}
	
	public static DHTNetworkPositionProvider
	getProvider(
		byte		type )
	{
		synchronized( providers_lock ){

			for (int i=0;i<providers.length;i++){
				
				if ( providers[i].getPositionType() == type ){
					
					return( providers[i] );
				}
			}
		}
		
		return( null );
	}
	
	public static DHTNetworkPosition[]
	getLocalPositions()
	{
		DHTNetworkPositionProvider[]	prov = providers;
		
		List<DHTNetworkPosition> res = new ArrayList<DHTNetworkPosition>();
		
		for (int i=0;i<prov.length;i++){
			
			try{
				DHTNetworkPosition	pos = prov[i].getLocalPosition();
				
				if ( pos != null ){
					
					res.add( pos );
				}
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
		
		return( res.toArray(new DHTNetworkPosition[res.size()])); 

	}
	
	public static DHTNetworkPosition
	getBestLocalPosition()
	{
		DHTNetworkPosition	best_position = null;
		
		DHTNetworkPosition[]	positions = getLocalPositions();
		
		byte	best_provider = DHTNetworkPosition.POSITION_TYPE_NONE;

		for (int i=0;i<positions.length;i++){
			
			DHTNetworkPosition	position = positions[i];
			
			int	type = position.getPositionType();
			
			if ( type > best_provider ){
				
				best_position = position;
			}
		}
		
		return( best_position );
	}
	
	public static DHTNetworkPosition[]
	createPositions(
		byte[]		ID,
		boolean		is_local )
	{
		DHTNetworkPositionProvider[]	prov = providers;
		
		if ( prov.length == 0 ){
		
			return( NP_EMPTY_ARRAY );
			
		}else{
			DHTNetworkPosition[]	res = new DHTNetworkPosition[prov.length];
			
			int	skipped	= 0;
			
			for (int i=0;i<res.length;i++){
				
				try{
					res[i] = prov[i].create( ID, is_local );
					
				}catch( Throwable e ){
					
					Debug.printStackTrace(e);
					
					skipped++;
				}
			}
			
			if  ( skipped > 0 ){
				
				DHTNetworkPosition[] x	= new DHTNetworkPosition[ res.length - skipped ];
				
				int	pos = 0;
				
				for (int i=0;i<res.length;i++){
					
					if ( res[i] != null ){
						
						x[pos++] = res[i];
					}
				}
				
				res	= x;
				
				if ( res.length == 0 ){
					
					Debug.out( "hmm" );
				}
			}
			
			return( res );
		}
	}
	
	public static float
	estimateRTT(
		DHTNetworkPosition[]		p1s,
		DHTNetworkPosition[]		p2s )
	{
		byte	best_provider = DHTNetworkPosition.POSITION_TYPE_NONE;
		
		float	best_result	= Float.NaN;
		
		for (int i=0;i<p1s.length;i++){
			
			DHTNetworkPosition	p1 = p1s[i];
			
			byte	p1_type = p1.getPositionType();
			
			for (int j=0;j<p2s.length;j++){
				
				DHTNetworkPosition	p2 = p2s[j];
				
				if ( p1_type == p2.getPositionType()){
					
					try{
						float	f = p1.estimateRTT( p2 );
						
						if ( !Float.isNaN( f )){
							
							if ( p1_type > best_provider ){
								
								best_result		= f;
								best_provider	= p1_type;
							}
						}
					}catch( Throwable e ){
						
						Debug.printStackTrace(e);
					}
					
					break;
				}
			}
		}
		
		return( best_result );
	}
	
	public static void
	update(
		DHTNetworkPosition[]	local_positions,
		byte[]					remote_id,
		DHTNetworkPosition[]	remote_positions,
		float					rtt )
	{	
		for (int i=0;i<local_positions.length;i++){
			
			DHTNetworkPosition	p1 = local_positions[i];
						
			for (int j=0;j<remote_positions.length;j++){
				
				DHTNetworkPosition	p2 = remote_positions[j];
				
				if ( p1.getPositionType() == p2.getPositionType()){
					
					try{
						p1.update( remote_id, p2, rtt );
						
					}catch( Throwable e ){
						
						Debug.printStackTrace(e);
					}
					
					break;
				}
			}
		}
	}
	
	public static byte[]
	serialisePosition(
		DHTNetworkPosition	pos )
	
		throws IOException
	{
		ByteArrayOutputStream	baos = new ByteArrayOutputStream();
		
		DataOutputStream	dos = new DataOutputStream( baos );
	
		dos.writeByte( 1 );	// version
		dos.writeByte( pos.getPositionType());
		
		pos.serialise( dos );
		
		dos.close();
		
		return( baos.toByteArray());
	}
	
	public static DHTNetworkPosition
   	deserialisePosition(
   		InetAddress				originator,
   		byte[]					bytes )
   	
   		throws IOException
   	{
   		ByteArrayInputStream	bais = new ByteArrayInputStream( bytes );
   		
   		DataInputStream	dis = new DataInputStream( bais );
   	
   		dis.readByte();	// version
   		
   		byte	position_type = dis.readByte();
   		
   		return( deserialise( originator, position_type, dis ));
   	}
	
	public static DHTNetworkPosition
	deserialise(
		InetAddress				originator,
		byte					position_type,
		DataInputStream			is )
	
		throws IOException
	{
		DHTNetworkPositionProvider[]	prov = providers;

		is.mark( 512 );

		for (int i=0;i<prov.length;i++){
			
			if ( prov[i].getPositionType() == position_type ){
				
				DHTNetworkPositionProvider	provider = prov[i];
				
				try{
					DHTNetworkPosition np = provider.deserialisePosition( is );
					
					CopyOnWriteList<DHTNetworkPositionListener> listeners = position_listeners;
					
					if ( listeners != null ){
						
						Iterator<DHTNetworkPositionListener> it = listeners.iterator();
						
						while( it.hasNext()){
							
							try{
								it.next().positionFound( provider, originator, np );
								
							}catch( Throwable e ){
								
								Debug.printStackTrace(e);
							}
						}
					}
					
					return( np );
					
				}catch( Throwable e ){
					
					Debug.printStackTrace(e);
					
					is.reset();
				}
				
				break;
			}
		}
		
		return( null );
	}
	
	public static void
	addPositionListener(
		DHTNetworkPositionListener		listener )
	{
		synchronized( DHTNetworkPositionManager.class ){
		
			if ( position_listeners == null ){
				
				position_listeners = new CopyOnWriteList<DHTNetworkPositionListener>();
			}
			
			position_listeners.add( listener );
		}
	}
	
	public static void
	removePositionListener(
		DHTNetworkPositionListener		listener )
	{
		synchronized( DHTNetworkPositionManager.class ){
		
			if ( position_listeners != null ){
				
				position_listeners.remove( listener );
				
				if ( position_listeners.size() == 0 ){
					
					position_listeners = null;
				}
			}
		}
	}
	
	public static void
	addProviderListener(
		DHTNetworkPositionProviderListener		listener )
	{
		provider_listeners.add( listener );
	}
	
	public static void
	removeProviderListener(
		DHTNetworkPositionProviderListener		listener )
	{
		provider_listeners.remove( listener );
	}
}