/*
 * Created on 03-Nov-2005
 * 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.clientmessageservice.secure.impl;

import java.security.interfaces.RSAPublicKey;

import java.util.*;

import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.AESemaphore;
import org.gudy.azureus2.core3.util.AEThread;
import org.gudy.azureus2.core3.util.SystemTime;

import com.aelitis.azureus.core.clientmessageservice.ClientMessageService;
import com.aelitis.azureus.core.clientmessageservice.secure.SecureMessageServiceClient;
import com.aelitis.azureus.core.clientmessageservice.secure.SecureMessageServiceClientAdapter;
import com.aelitis.azureus.core.clientmessageservice.secure.SecureMessageServiceClientListener;
import com.aelitis.azureus.core.clientmessageservice.secure.SecureMessageServiceClientMessage;

public class 
SecureMessageServiceClientImpl
	implements SecureMessageServiceClient
{
		// these also occur in the server
	
	public static final int STATUS_OK					= 0;
	public static final int STATUS_LOGON_FAIL			= 1;
	public static final int STATUS_INVALID_SEQUENCE		= 2;	
	public static final int STATUS_FAILED				= 3;
	public static final int STATUS_ABORT				= 4;

	public static final String	SERVICE_NAME	= "SecureMsgServ";
	
	private static final long	MIN_RETRY_PERIOD		=    5*60*1000;
	private static final long	MAX_RETRY_PERIOD		= 2*60*60*1000;

	private String									host;
	private int										port;
	private int										timeout_secs;
	private RSAPublicKey							public_key;
	private SecureMessageServiceClientAdapter		adapter;

	private long				retry_millis			= MIN_RETRY_PERIOD;
	private int					connect_failure_count	= 0;
	
	
	private AEMonitor			message_mon;
	private AESemaphore			message_sem;
		
	private String				last_failed_user_pw			= "";
	private long				last_failed_user_pw_time;
	
	private List				messages 	= new ArrayList();
	private List				listeners	= new ArrayList();
	
	public
	SecureMessageServiceClientImpl(
		String								_host,
		int									_port,
		int									_timeout_secs,
		RSAPublicKey						_key,
		SecureMessageServiceClientAdapter	_adapter )	
	{
		host			= _host;
		port			= _port;
		timeout_secs	= _timeout_secs;
		public_key		= _key;
		adapter			= _adapter;
				
		message_mon	= new AEMonitor( "SecureService:messages" );
		
		message_sem = new AESemaphore( "SecureService:messages" );
		
		new AEThread( "SecureService::messageSender", true )
		{
			public void
			runSupport()
			{
				while( true ){
					
					long	time = retry_millis;
					
					if ( connect_failure_count > 0 ){
						
						for (int i=0;i<connect_failure_count;i++){
							
							time = time + time;
							
							if ( time > MAX_RETRY_PERIOD ){
								
								time = MAX_RETRY_PERIOD;
								
								break;
							}
						}
					}
					
					message_sem.reserve( time );
					
					try{
						sendMessagesSupport();
						
					}catch( Throwable e ){
						
						adapter.log( "Request processing failed", e);
					}
				}
			}
		}.start();
	}
	
	public void
	sendMessages()
	{
		message_sem.release();
	}
	
	protected void
	sendMessagesSupport()
	{	
		String	user 		= adapter.getUser();
		byte[]	password	= adapter.getPassword();
		
		String	user_password = user + "/" + new String( password );
		
			// user name must be defined, however we allow a blank password
		
		if ( user.length() == 0 ){
			
			adapter.authenticationFailed();
			
			return;
		}
		
			// if user-name + password hasn't changed recently and logon failed then
			// don't re-attempt
		
		if ( user_password.equals( last_failed_user_pw )){
			
			final long now =SystemTime.getCurrentTime();

            if (now >last_failed_user_pw_time &&now -last_failed_user_pw_time <60 *1000){

				adapter.authenticationFailed();

				return;
			}
		}
		
		List	outstanding_messages;
		
		try{
			message_mon.enter();

			outstanding_messages	= new ArrayList( messages );
			
		}finally{
			
			message_mon.exit();
		}
		
		if ( outstanding_messages.size() == 0 ){
			
			return;
		}
		
		List	complete_messages	= new ArrayList();
		
		boolean	failed = false;
		
		try{
			Iterator	it = outstanding_messages.iterator();
			
			while( it.hasNext() && !failed ){
				
				SecureMessageServiceClientMessageImpl	message = (SecureMessageServiceClientMessageImpl)it.next();
					
				boolean	retry 			= true;
				int		retry_count		= 0;
				
				while( retry && !failed ){
					
					retry	= false;
					
					ClientMessageService	message_service = null;

					boolean	got_reply = false;
					
					try{
						Map	content	= new HashMap();				
			
						long	sequence = adapter.getMessageSequence();
						
						content.put( "user", 		user );
						content.put( "password", 	password );
						content.put( "seq", 		new Long( sequence ));
						content.put( "request", 	message.getRequest());
							
						last_failed_user_pw = "";
						
						message_service = SecureMessageServiceClientHelper.getServerService( host, port, timeout_secs, SERVICE_NAME, public_key );					

						message_service.sendMessage( content );
						
						Map	reply = message_service.receiveMessage();
						
						got_reply	= true;
							
						long	status = ((Long)reply.get( "status" )).longValue();
						
						Long	new_retry = (Long)reply.get( "retry" );
						
						if ( new_retry != null ){
							
							retry_millis = new_retry.longValue();
							
							if ( retry_millis < MIN_RETRY_PERIOD ){
								
								retry_millis = MIN_RETRY_PERIOD;
							}
							
							adapter.log( "Server requested retry period of " + (retry_millis/1000) + " seconds" );
							
						}else{
							
							retry_millis = MIN_RETRY_PERIOD;
						}
						
						if ( status == STATUS_OK ){
		
							message.setReply( (Map)reply.get( "reply" ));

							adapter.log( "Request successfully sent: " + message.getRequest() + "->" + message.getReply());							
							
							adapter.setMessageSequence( sequence + 1 );
							
							adapter.serverOK();
	
							for (Iterator l_it=listeners.iterator();l_it.hasNext();){
								
								try{
									((SecureMessageServiceClientListener)l_it.next()).complete( message );
									
								}catch( Throwable e ){
									
									e.printStackTrace();
								}
							}
							
							complete_messages.add( message );
							
						}else if ( status == STATUS_LOGON_FAIL ){
							
							last_failed_user_pw 		= user_password;
							last_failed_user_pw_time	= SystemTime.getCurrentTime();
							
							adapter.serverOK();
							
							adapter.authenticationFailed();
							
							failed	= true;
						
						}else if ( status == STATUS_INVALID_SEQUENCE ){
							
							if ( retry_count == 1 ){
																
								adapter.serverFailed( new Exception( "Sequence resynchronisation failed" ));
								
								failed = true;
								
							}else{
							
								retry_count++;
								
								retry	= true;
								
								long	expected_sequence = ((Long)reply.get( "seq" )).longValue();
								
								adapter.log( "Sequence resynchronise: local = " + sequence + ", remote = " + expected_sequence );
								
								adapter.setMessageSequence( expected_sequence );
							}

						}else if ( status == STATUS_FAILED ){

							adapter.serverFailed( new Exception( new String( (byte[])reply.get( "error" ))));
							
							failed = true;
							
						}else{//  if ( status == STATUS_ABORT ){
						
								// this is when things have gone badly wrong server-side - we just
								// dump the message
							
							adapter.serverFailed( new Exception( "Server requested abort" ));

							for (Iterator l_it=listeners.iterator();l_it.hasNext();){
								
								try{
									((SecureMessageServiceClientListener)l_it.next()).aborted( 
											message,
											new String( (byte[])reply.get( "error" )));
									
								}catch( Throwable e ){
									
									e.printStackTrace();
								}
							}
							
							complete_messages.add( message );							
						}
						
					}catch( Throwable e ){
									
						adapter.serverFailed( e );
						
						failed	= true;
						
					}finally{
						
						if ( got_reply ){
							
							connect_failure_count = 0;
							
						}else{
							
							connect_failure_count++;
							
							if ( connect_failure_count > 1 ){
								
								try{
									adapter.log( "Failed to contact server " + connect_failure_count + " times in a row" );
									
								}catch( Throwable e ){
									
									e.printStackTrace();
								}
							}
						}
						
						if ( message_service != null ){
							
							message_service.close();
						}
					}
				}
			}
		}catch( Throwable e ){
			
			adapter.serverFailed( e );
			
		}finally{
			
			try{
				message_mon.enter();
					
				messages.removeAll( complete_messages );
				
			}finally{
				
				message_mon.exit();
			}
		}
	}
	
	public SecureMessageServiceClientMessage
	sendMessage(
		Map			request,
		Object		data,
		String		description )
	{
		try{
			message_mon.enter();
			
			SecureMessageServiceClientMessage	res =  new SecureMessageServiceClientMessageImpl( this, request, data, description );
			
			messages.add( res );
			
			message_sem.release();
			
			return( res );
			
		}finally{
			
			message_mon.exit();
		}
	}
	
	protected void
	cancel(
		SecureMessageServiceClientMessage	message )
	{
		boolean	inform	= false;
		
		try{
			message_mon.enter();
			
			if ( messages.remove( message )){
				
				inform	= true;
			}
		}finally{
			
			message_mon.exit();
		}
		
		if ( inform ){
			
			for (Iterator it=listeners.iterator();it.hasNext();){
				
				try{
					((SecureMessageServiceClientListener)it.next()).cancelled( message );
					
				}catch( Throwable e ){
					
					e.printStackTrace();
				}
			}
		}
	}
	
	public SecureMessageServiceClientMessage[]
	getMessages()
	{
		try{
			message_mon.enter();
			
			return((SecureMessageServiceClientMessage[])messages.toArray( new SecureMessageServiceClientMessage[ messages.size()]));
			
		}finally{
			
			message_mon.exit();
		}	
	}
	
	public void
	addListener(
		SecureMessageServiceClientListener	l )
	{
		listeners.add( l );
	}
	
	public void
	removeListener(
		SecureMessageServiceClientListener	l )
	{
		listeners.remove( l );
	}
}
