/*
 * Created on Apr 23, 2008
 * Created by Paul Gardner
 * 
 * Copyright 2008 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 com.aelitis.azureus.plugins.net.buddy;

import java.io.File;
import java.util.*;

import org.gudy.azureus2.core3.util.BDecoder;
import org.gudy.azureus2.core3.util.BEncoder;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.SystemTime;

import com.aelitis.azureus.core.util.CopyOnWriteList;

public class 
BuddyPluginBuddyMessageHandler 
{
	private BuddyPluginBuddy		buddy;
	private File					store;
	
	private Map	config_map;
	private int	message_count;
	private int pending_deletes;
	
	private int	next_message_id;
	
	private CopyOnWriteList			listeners = new CopyOnWriteList();
	
	private BuddyPluginBuddyMessage	active_message;
	
	private long					last_failure;
	private long					last_pending_success;
	
	
	protected
	BuddyPluginBuddyMessageHandler(
		BuddyPluginBuddy		_buddy,
		File					_store )
	{
		buddy	= _buddy;
		store	= _store;
		
		loadConfig();
		
		if ( message_count > 0 ){
			
			buddy.persistentDispatchPending();
		}
	}
	
	public BuddyPluginBuddy
	getBuddy()
	{
		return( buddy );
	}
	
	public BuddyPluginBuddyMessage
	queueMessage(
		int		subsystem,
		Map		content,
		int		timeout_millis )
	
		throws BuddyPluginException
	{
		BuddyPluginBuddyMessage	message;
		
		boolean	dispatch_pending;
		
		synchronized( this ){
			
			int	id = next_message_id++;
			
			message = 
				new BuddyPluginBuddyMessage( 
						this, id, subsystem, content, timeout_millis, SystemTime.getCurrentTime());
		
			storeMessage( message );
			
			dispatch_pending = message_count == 1;
		}
		
		Iterator it = listeners.iterator();
		
		while( it.hasNext()){
			
			try{
				((BuddyPluginBuddyMessageListener)it.next()).messageQueued( message );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
		
		if ( dispatch_pending ){
			
			buddy.persistentDispatchPending();
		}
		
		return( message );
	}	
	
	protected void
	checkPersistentDispatch()
	{
		boolean	request_dispatch = false;
		
		synchronized( this ){

			long	now = SystemTime.getCurrentTime();

			if ( now < last_failure ){
				
				last_failure = now;
			}

			if ( now < last_pending_success ){
				
				last_pending_success = now;
			}

			if ( last_pending_success > 0 && now - last_pending_success >= BuddyPlugin.PERSISTENT_MSG_RETRY_PERIOD ){
				
				request_dispatch = true;
				
			}else if ( active_message != null || message_count == 0 || last_failure == 0 ){
				
					// no messages pending
				
			}else{
				
				request_dispatch = now - last_failure >= BuddyPlugin.PERSISTENT_MSG_RETRY_PERIOD;
			}
		}
		
		if ( request_dispatch ){
			
			buddy.persistentDispatchPending();
		}
	}
	
	protected void
	persistentDispatch()
	{
		checkPendingSuccess();
		
		synchronized( this ){

			if ( active_message != null || message_count == 0 ){
				
				return;
			}
			
			List	messages = (List)config_map.get( "messages" );

			Map		map = (Map)messages.get(0);
			
			try{
				active_message = restoreMessage( map );
				
			}catch( Throwable e ){
				
					// should never happen...
				
				Debug.out( "Failed to restore message, deleting it", e );
				
				messages.remove(0);
								
				try{
					saveConfig();
					
				}catch( Throwable f ){
					
					buddy.log( "Config save failed during delete of bad message", f );
				}
			}
		}
		
		boolean	request_ok = false;
		
		try{
			Map	request = active_message.getRequest();
		
			request_ok = true;
			
			buddy.sendMessage(
					active_message.getSubsystem(),
					request,
					active_message.getTimeout(),
					new BuddyPluginBuddyReplyListener()
					{
						public void
						replyReceived(
							BuddyPluginBuddy		from_buddy,
							Map						reply )
						{
							BuddyPluginBuddyMessage message = active_message;
							
								// inform listeners before deleting message as it gives them one
								// last chance to do something with the message if they so desire
							
							Iterator it = listeners.iterator();
							
							boolean	processing_ok = true;
							
								// prematurely reduce message count when informing listeners
								// so they see the "correct" value
							
							try{
								synchronized( BuddyPluginBuddyMessageHandler.this ){
									
									pending_deletes++;
								}
								
								while( it.hasNext()){
									
									try{
										if ( !((BuddyPluginBuddyMessageListener)it.next()).deliverySucceeded( message, reply )){
											
											processing_ok = false;
										}
										
									}catch( Throwable e ){
										
										Debug.printStackTrace(e);
									}
								}
							}finally{
								
								synchronized( BuddyPluginBuddyMessageHandler.this ){
									
									pending_deletes--;
								}
							}
							if ( processing_ok ){
							
								message.delete();
								
							}else{
								synchronized( BuddyPluginBuddyMessageHandler.this ){
									
									boolean found = false;
									
									List	messages = (List)config_map.get( "messages" );

									if ( messages != null ){
										
										for ( int i=0;i<messages.size();i++){
											
											Map	msg = (Map)messages.get(i);
											
											if ( message.getID() == ((Long)msg.get( "id")).intValue()){
												
												found = true;
												
												messages.remove(i);
												
												try{
													writeReply( message, reply );

													List pending_success = (List)config_map.get( "pending_success" );
													
													if ( pending_success == null ){
														
														pending_success = new ArrayList();
														
														config_map.put( "pending_success", pending_success );
													}
													
													pending_success.add( msg );
													
													last_pending_success = SystemTime.getCurrentTime();
													
													buddy.log( "Message moved to pending success queue after listener failed" );
													
													saveConfig();
													
												}catch( Throwable e ){
													
													buddy.log( "Config save failed during message pending queueing", e );
												}
												
												break;
											}
										}	
									}
									
									if ( !found ){
																				
										buddy.log( "Failed to find message " + message.getID());
									}
								}
							}
							
							boolean messages_queued;
							
							synchronized( BuddyPluginBuddyMessageHandler.this ){

								active_message 	= null;
								
								messages_queued = message_count > 0;
								
								last_failure	= 0;
							}
															
							if ( messages_queued ){
							
								buddy.persistentDispatchPending();
							}
						}
						
						public void
						sendFailed(
							BuddyPluginBuddy		to_buddy,
							BuddyPluginException	cause )
						{
							BuddyPluginBuddyMessage message = active_message;
							
							synchronized( BuddyPluginBuddyMessageHandler.this ){

								active_message 	= null;
								
								last_failure	= SystemTime.getCurrentTime();
							}
							
							reportFailed( message, cause, true );
						}
					});
					
		}catch( Throwable cause ){
			
			BuddyPluginBuddyMessage message = active_message;

			synchronized( this ){

				active_message 	= null;
				
				last_failure	= SystemTime.getCurrentTime();
			}
			
			boolean do_subsequent = true;
			
			if ( !request_ok && !( cause instanceof BuddyPluginPasswordException )){
				
				buddy.logMessage( "Message request unavailable, deleting message" );
				
				message.delete();
				
				boolean messages_queued = false;
				
				synchronized( this ){

					last_failure = 0;
					
					messages_queued = message_count > 0;
				}
					
				if ( messages_queued ){
				
					do_subsequent = false;
					
					buddy.persistentDispatchPending();
				}
			}
			
			reportFailed( message, cause, do_subsequent );
		}
	}
	
	protected void
	reportFailed(
		BuddyPluginBuddyMessage		message,
		Throwable					cause,
		boolean						do_subsequent )
	{	
		BuddyPluginException b_cause;
		
		if ( cause instanceof BuddyPluginException ){
			
			b_cause = (BuddyPluginException)cause;
			
		}else{
			
			b_cause = new BuddyPluginException( "Failed to send message", cause );
		}
		
		reportFailedSupport( message, b_cause );
		
		if ( do_subsequent ){
			
			List	other_messages = new ArrayList();
			
			synchronized( this ){
	
				List	messages = (List)config_map.get( "messages" );
	
				for (int i=0;i<messages.size();i++){
				
					try{
						BuddyPluginBuddyMessage msg = restoreMessage((Map)messages.get(i));
	
						if ( msg.getID() != message.getID()){
							
							other_messages.add( msg );
						}
					}catch( Throwable e ){
						
					}
				}
			}
	
			if ( other_messages.size() > 0 ){
				
				BuddyPluginException o_cause = new BuddyPluginException( "Reporting probable failure to subsequent messages" );
				
				for (int i=0;i<other_messages.size();i++){
					
					reportFailedSupport((BuddyPluginBuddyMessage)other_messages.get(i), o_cause );
				}
			}
		}
	}
	
	protected void
	reportFailedSupport(
		BuddyPluginBuddyMessage		message,
		BuddyPluginException		cause )
	{	
		Iterator it = listeners.iterator();
	
		while( it.hasNext()){
			
			try{
				((BuddyPluginBuddyMessageListener)it.next()).deliveryFailed( message, cause );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}
	
	protected void
	checkPendingSuccess()
	{
		last_pending_success	= 0;
		
		List	pending_messages = new ArrayList();
		
		boolean	save_pending = false;
		
		synchronized( this ){

			List pending_success = (List)config_map.get( "pending_success" );

			if ( pending_success == null || pending_success.size() == 0 ){
				
				return;
			}
			
			Iterator it = pending_success.iterator();
			
			while( it.hasNext()){
				
				Map	map = (Map)it.next();
			
				try{
					pending_messages.add( restoreMessage( map ));
					
				}catch( Throwable e ){
					
					buddy.log( "Failed to restore message from pending success queue", e );
					
					it.remove();
					
					save_pending = true;
				}
			}
		}
		
		for ( int i=0;i<pending_messages.size();i++){
	
			BuddyPluginBuddyMessage message = (BuddyPluginBuddyMessage)pending_messages.get(i);
			
			try{
				Map	reply = message.getReply();
				
				Iterator it = listeners.iterator();
				
				boolean	processing_ok = true;
				
				while( it.hasNext()){
					
					try{
						if ( !((BuddyPluginBuddyMessageListener)it.next()).deliverySucceeded( message, reply )){
							
							processing_ok = false;
						}
						
					}catch( Throwable e ){
						
						Debug.printStackTrace(e);
					}
				}
	
				if ( processing_ok ){
				
					message.delete();
					
				}else{
					synchronized( this ){
	
						last_pending_success = SystemTime.getCurrentTime();
					}
				}
			}catch( BuddyPluginPasswordException e ){
				
				buddy.log( "Failed to restore message reply", e );

					// we don't want to delete the message if failed due to password issue
				
			}catch( Throwable e ){
				
				buddy.log( "Failed to restore message reply - deleting message", e );

				message.delete();
			}
		}
		
		if ( save_pending ){
			
			try{
				saveConfig();
				
			}catch( Throwable e ){
				
				buddy.log( "Save failed during pending success processing", e );
			}
		}
	}
	
	public int
	getMessageCount()
	{
		synchronized( this ){
		
			return( message_count - pending_deletes );
		}
	}
	
	protected void
	deleteMessage(
		BuddyPluginBuddyMessage		message )
	{
		Iterator it = listeners.iterator();
		
		while( it.hasNext()){
			
			try{
				((BuddyPluginBuddyMessageListener)it.next()).messageDeleted( message );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
		
		synchronized( this ){
			
			String[]	keys = { "messages", "pending_success", "explicit" };
			
			for (int i=0;i<keys.length;i++){
				
				List	messages = (List)config_map.get( keys[i] );
	
				if ( messages != null ){
	
					boolean	found = false;
					
					for ( int j=0;j<messages.size();j++){
						
						Map	msg = (Map)messages.get(j);
						
						if ( message.getID() == ((Long)msg.get( "id")).intValue()){
							
							messages.remove(j);
							
							found	= true;
							
							break;
						}
					}
				
					if ( found ){
					
						deleteRequest( message );
						
						deleteReply( message );
						
						try{
							saveConfig();
							
						}catch( Throwable e ){
							
							buddy.log( "Config save failed during message delete", e );
						}
						
						return;
					}
				}
			}
		}
	}
	
	protected void
	destroy()
	{
		synchronized( this ){
			
			config_map.clear();
			
			try{
				saveConfig();
				
			}catch( Throwable e ){
				
				buddy.log( "Config save failed during destroy", e );
			}
		}
	}
	
	protected void
	writeRequest(
		BuddyPluginBuddyMessage		message,
		Map							content )
	
		throws BuddyPluginException
	{
		writeContent( message.getID() + ".req.dat", content );
	}
	
	protected Map
	readRequest(
		BuddyPluginBuddyMessage		message )
	
		throws BuddyPluginException
	{		
		return( readContent( message.getID() + ".req.dat" ));
	}
	
	protected void
	writeReply(
		BuddyPluginBuddyMessage		message,
		Map							content )
	
		throws BuddyPluginException
	{
		writeContent( message.getID() + ".rep.dat", content );
	}
	
	protected Map
	readReply(
		BuddyPluginBuddyMessage		message )
	
		throws BuddyPluginException
	{		
		return( readContent( message.getID() + ".rep.dat" ));
	}
	
	protected void
	writeContent(
		String						target_str,
		Map							content )
	
		throws BuddyPluginException
	{
		if ( !store.exists()){
			
			if ( !store.mkdirs()){
				
				throw( new BuddyPluginException( "Failed to create " + store ));
			}
		}
		
		File target = new File( store, target_str );
		
		try{
		
			BuddyPlugin.cryptoResult result = buddy.encrypt( BEncoder.encode( content ));
			
			Map	store_map = new HashMap();
			
			store_map.put( "pk", buddy.getPlugin().getPublicKey());
			store_map.put( "data", result.getPayload());
			
			if ( !buddy.writeConfigFile( target, store_map )){
				
				throw( new BuddyPluginException( "failed to write " + target ));
			}
			
		}catch( BuddyPluginException e ){
			
			throw( e );
			
		}catch( Throwable e ){
			
			throw( new BuddyPluginException( "Failed to write message", e ));
		}
	}
	
	protected Map
	readContent(
		String						target_str )
	
		throws BuddyPluginException
	{		
		File target = new File( store, target_str );

		if ( !target.exists()){

			throw( new BuddyPluginException( "Failed to read persisted message - " + target + " doesn't exist" ));
		}

		Map	map = buddy.readConfigFile( target );
		
		if ( map.size() == 0 ){
			
			throw( new BuddyPluginException( "Failed to read persisted message file " + target ));
		}
		
		try{
			String	pk = new String((byte[])map.get("pk"));
			
			if ( !pk.equals( buddy.getPlugin().getPublicKey())){
				
				throw( new BuddyPluginException( "Can't decrypt message as key changed" ));
			}
			
			byte[]	data = (byte[])map.get( "data" );
			
			return( BDecoder.decode( buddy.decrypt( data ).getPayload()));
			
		}catch( BuddyPluginException e ){
			
			throw( e );
			
		}catch( Throwable e ){
			
			throw( new BuddyPluginException( "Failed to read message", e ));
		}
	}
	
	protected void
	deleteRequest(
		BuddyPluginBuddyMessage		message )
	{
		deleteRequest( message.getID());
	}
	
	protected void
	deleteRequest(
		int			id  )
	{	
		File target = new File( store, id + ".req.dat" );
		
		if ( target.exists()){
			
			if ( !target.delete()){
				
				Debug.out( "Failed to delete " + target );
			}
		}
	}
	
	protected void
	deleteReply(
		BuddyPluginBuddyMessage		message )
	{
		deleteReply( message.getID());
	}
	
	protected void
	deleteReply(
		int			id  )
	{	
		File target = new File( store, id + ".rep.dat" );
		
		if ( target.exists()){
			
			if ( !target.delete()){
				
				Debug.out( "Failed to delete " + target );
			}
		}
	}
	
	public BuddyPluginBuddyMessage
	storeExplicitMessage(
		int		type,
		Map		msg )
	{
		BuddyPluginBuddyMessage	message;
				
		synchronized( this ){
			
			int	id = next_message_id++;
			
			try{
				message = 
					new BuddyPluginBuddyMessage( 
							this, id, BuddyPlugin.SUBSYSTEM_MSG_TYPE_BASE + type, msg, 0, SystemTime.getCurrentTime());
			
				storeExplicitMessage( message );
				
			}catch( Throwable e ){
				
				buddy.log( "Failed to store explicit message", e );
				
				return( null );
			}
		}
		
		return( message );
	}
	
	public List<BuddyPluginBuddyMessage>
	retrieveExplicitMessages(
		int		type )
	{
		List<BuddyPluginBuddyMessage>	result = new ArrayList<BuddyPluginBuddyMessage>();
		
		synchronized( this ){

			List<Map<String,Object>>	messages = (List<Map<String,Object>>)config_map.get( "explicit" );
			
			if ( messages != null ){
				
				for (int i=0;i<messages.size();i++){
					
					try{
						BuddyPluginBuddyMessage msg = restoreMessage(messages.get(i));
						
						if ( msg.getSubsystem() == BuddyPlugin.SUBSYSTEM_MSG_TYPE_BASE + type ){
							
							result.add( msg );
						}
					}catch( Throwable e ){
						
						buddy.log( "Failed to restore message", e );
					}
				}
			}
		}
		
		return( result );
	}
	
	protected void
	storeExplicitMessage(
		BuddyPluginBuddyMessage		msg )
	
		throws BuddyPluginException
	{
		storeMessageSupport( msg, "explicit" );
	}
	
	protected void
	storeMessage(
		BuddyPluginBuddyMessage		msg )
	
		throws BuddyPluginException
	{
		storeMessageSupport( msg, "messages" );
	}
	
	protected void
	storeMessageSupport(
		BuddyPluginBuddyMessage		msg,
		String						key )
	
		throws BuddyPluginException
	{
		List	messages = (List)config_map.get( key );

		if ( messages == null ){
			
			messages = new ArrayList();
			
			config_map.put( key, messages );
		}
		
		Map map = new HashMap();
		
		map.put( "id", new Long( msg.getID()));
		map.put( "ss", new Long( msg.getSubsystem()));
		map.put( "to", new Long( msg.getTimeout()));
		map.put( "cr", new Long( msg.getCreateTime()));
		
		messages.add( map );
				
		saveConfig();
	}
	
	protected BuddyPluginBuddyMessage
	restoreMessage(
		Map			map )
	
		throws BuddyPluginException
	{
		int	id = ((Long)map.get( "id" )).intValue();
		int	ss = ((Long)map.get( "ss" )).intValue();
		int	to = ((Long)map.get( "to" )).intValue();
		
		long	cr = ((Long)map.get( "cr" )).longValue();
		
		return( new BuddyPluginBuddyMessage( this, id, ss, null, to, cr ));
	}
	
	protected void
	loadConfig()
	{
		File	config_file = new File( store, "messages.dat" );
		
		if ( config_file.exists()){
			
			config_map = buddy.readConfigFile( config_file );
			
		}else{
			
			config_map = new HashMap();
		}
		
		List	messages = (List)config_map.get( "messages" );
		
		if ( messages != null ){
			
			message_count = messages.size();
			
			if ( message_count > 0 ){
				
				Map	last_msg = (Map)messages.get( message_count - 1 );
				
				next_message_id = ((Long)last_msg.get( "id")).intValue() + 1;
			}
		}
		
		List	pending_success = (List)config_map.get( "pending_success" );
		
		if ( pending_success != null ){
			
			int ps_count = pending_success.size();
		
			if ( ps_count > 0 ){
										
				Map	last_msg = (Map)pending_success.get( ps_count - 1 );
					
				next_message_id = Math.max( next_message_id, ((Long)last_msg.get( "id")).intValue() + 1 );
				
				last_pending_success = SystemTime.getCurrentTime();
			}
		}
		
		List	explicit = (List)config_map.get( "explicit" );
		
		if ( explicit != null ){
			
			int exp_count = explicit.size();
		
			if ( exp_count > 0 ){
										
				Map	last_msg = (Map)explicit.get( exp_count - 1 );
					
				next_message_id = Math.max( next_message_id, ((Long)last_msg.get( "id")).intValue() + 1 );
			}
		}
	}
	
	protected void
	saveConfig()
	
		throws BuddyPluginException
	{
		File	config_file = new File( store, "messages.dat" );
		
		List	messages 	= (List)config_map.get( "messages" );
		List	pending 	= (List)config_map.get( "pending_success" );
		List	explicit 	= (List)config_map.get( "explicit" );

		if ( 	( messages == null || messages.size() == 0 ) && 
				( pending == null || pending.size() == 0 ) && 
				( explicit == null || explicit.size() == 0 )){
			
			if ( store.exists()){
			
				File[]	 files = store.listFiles();
			
				for (int i=0;i<files.length;i++ ){
					
					files[i].delete();
				}
				
				store.delete();
			}
			
			message_count = 0;
			
			next_message_id	= 0;
			
		}else{
			
			if ( !store.exists()){
				
				if ( !store.mkdirs()){
					
					throw( new BuddyPluginException( "Failed to create " + store ));
				}
			}
			
			if ( !buddy.writeConfigFile( config_file, config_map )){
				
				throw( new BuddyPluginException( "Failed to write" + config_file ));
			}
			
			message_count = messages==null?0:messages.size();
		}
	}
	
	public void
	addListener(
		BuddyPluginBuddyMessageListener		listener )
	{
		listeners.add( listener );
	}
	
	public void
	removeListener(
		BuddyPluginBuddyMessageListener		listener )
	{
		listeners.remove( listener );
	}
}
