/*
 * Created on Apr 10, 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.security.SecureRandom;
import java.util.*;

import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.Base32;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.torrent.Torrent;
import org.gudy.azureus2.plugins.ui.UIManagerEvent;

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

public class 
BuddyPluginAZ2 
{
	public static final int RT_AZ2_REQUEST_MESSAGE		= 1;
	public static final int RT_AZ2_REPLY_MESSAGE		= 2;
	
	public static final int RT_AZ2_REQUEST_SEND_TORRENT	= 3;
	public static final int RT_AZ2_REPLY_SEND_TORRENT	= 4;

	public static final int RT_AZ2_REQUEST_CHAT			= 5;
	public static final int RT_AZ2_REPLY_CHAT			= 6;

	public static final int RT_AZ2_REQUEST_TRACK		= 7;
	public static final int RT_AZ2_REPLY_TRACK			= 8;

	public static final int RT_AZ2_REQUEST_RSS			= 9;
	public static final int RT_AZ2_REPLY_RSS			= 10;

	public static final int CHAT_MSG_TYPE_TEXT						= 1;
	public static final int CHAT_MSG_TYPE_PARTICIPANTS_ADDED		= 2;
	public static final int CHAT_MSG_TYPE_PARTICIPANTS_REMOVED		= 3;
	

	private static final int SEND_TIMEOUT = 2*60*1000;
	
	private BuddyPlugin		plugin;
	
	private Map				chats 		= new HashMap();
	
	private CopyOnWriteList	listeners = new CopyOnWriteList();
	
	private CopyOnWriteList	track_listeners = new CopyOnWriteList();
	
	protected 
	BuddyPluginAZ2(
		BuddyPlugin		_plugin )
	{
		plugin	= _plugin;
		
		plugin.addRequestListener(
				new BuddyPluginBuddyRequestListener()
				{
					public Map
					requestReceived(
						BuddyPluginBuddy	from_buddy,
						int					subsystem,
						Map					request )
					
						throws BuddyPluginException
					{
						if ( subsystem == BuddyPlugin.SUBSYSTEM_AZ2 ){
							
							if ( !from_buddy.isAuthorised()){
							
								throw( new BuddyPluginException( "Unauthorised" ));
							}
						
							return( processAZ2Request( from_buddy, request ));
						}

						return( null );
					}
					
					public void
					pendingMessages(
						BuddyPluginBuddy[]	from_buddies )
					{
					}
				});
	}
	
	protected Map
	processAZ2Request(
		final BuddyPluginBuddy	from_buddy,
		Map						request )		
		
		throws BuddyPluginException
	{
		logMessage( "AZ2 request received: " + from_buddy.getString() + " -> " + request );
			
		int	type = ((Long)request.get( "type" )).intValue();
		
		Map	reply = new HashMap();
				
		if ( type == RT_AZ2_REQUEST_MESSAGE ){
			
			try{
				String	msg = new String( (byte[])request.get( "msg" ), "UTF8" );
			
				from_buddy.setLastMessageReceived( msg );
				
			}catch( Throwable e ){
				
			}
			
			reply.put( "type", new Long( RT_AZ2_REPLY_MESSAGE ));

		}else if (  type == RT_AZ2_REQUEST_SEND_TORRENT ){
			
			try{
				final Torrent	torrent = plugin.getPluginInterface().getTorrentManager().createFromBEncodedData((byte[])request.get( "torrent" ));
			
				new AEThread2( "torrentAdder", true )
				{
					public void
					run()
					{
						PluginInterface pi = plugin.getPluginInterface();
						
						String msg = pi.getUtilities().getLocaleUtilities().getLocalisedMessageText(
								"azbuddy.addtorrent.msg", 
								new String[]{ from_buddy.getName(), torrent.getName() });
						
						long res = pi.getUIManager().showMessageBox(
										"azbuddy.addtorrent.title",
										"!" + msg + "!",
										UIManagerEvent.MT_YES | UIManagerEvent.MT_NO );
						
						if ( res == UIManagerEvent.MT_YES ){
						
							pi.getUIManager().openTorrent( torrent );
						}
					}
				}.start();
				
				reply.put( "type", new Long( RT_AZ2_REPLY_SEND_TORRENT ));
				
			}catch( Throwable e ){
				
				throw( new BuddyPluginException( "Torrent receive failed " + type ));
			}
		}else if (  type == RT_AZ2_REQUEST_CHAT ){
			
			Map msg = (Map)request.get( "msg" );
			
			String	id = new String((byte[])msg.get( "id" ));
			
			chatInstance	chat;
			boolean			new_chat = false;
			
			synchronized( chats ){
				
				 chat = (chatInstance)chats.get( id );
				 
				 if ( chat == null ){
					 
					 if ( chats.size() > 32 ){
						 
						 throw( new BuddyPluginException( "Too many chats" ));
					 }
					 
					 chat = new chatInstance( id );
					 
					 chats.put( id, chat );
					 
					 new_chat = true;
				 }
			}
			
			if ( new_chat ){
			
				informCreated( chat );
			}
			
			chat.addParticipant( from_buddy );
			
			chat.process( from_buddy, msg );
						
			reply.put( "type", new Long( RT_AZ2_REPLY_CHAT ));
						
		}else if (  type == RT_AZ2_REQUEST_TRACK ){
			
			Map msg = (Map)request.get( "msg" );

			Iterator it = track_listeners.iterator();
			
			boolean	ok = false;
			
			while( it.hasNext()){
				
				try{
			
					Map res = ((BuddyPluginAZ2TrackerListener)it.next()).messageReceived( from_buddy, msg );
					
					if ( res != null ){
						
						reply.put( "msg", res );
						reply.put( "type", new Long( RT_AZ2_REPLY_TRACK ));

						ok = true;
						
						break;
					}
				}catch( Throwable e ){
					
					Debug.printStackTrace(e);
				}
			}
			
			if ( !ok ){
			
				throw( new BuddyPluginException( "Unhandled request type " + type ));
			}
		}else if (  type == RT_AZ2_REQUEST_RSS ){
				
			try{
				Map<String,Object> res = new HashMap<String, Object>();

				reply.put( "msg", res );
				reply.put( "type", new Long( RT_AZ2_REPLY_RSS ));

				Map msg = (Map)request.get( "msg" );

				String category = new String((byte[])msg.get( "cat"), "UTF-8" );
				
				byte[] hash	= (byte[])msg.get( "hash" );
				
				if ( hash == null ){
					
					byte[]	if_mod 	= (byte[])msg.get( "if_mod" );
					
					BuddyPlugin.feedDetails feed = plugin.getRSS( from_buddy, category, if_mod==null?null:new String( if_mod, "UTF-8" ));
					
					res.put( "rss", feed.getContent());
					
					res.put( "last_mod", feed.getLastModified());
					
				}else{
					
					res.put( "torrent", plugin.getRSSTorrent( from_buddy, category, hash ));
				}
			}catch( BuddyPluginException e  ){
				
				throw( e );
				
			}catch( Throwable e ){
				
				throw( new BuddyPluginException( "Failed to handle rss", e ));
			}
		}else{
			
			throw( new BuddyPluginException( "Unrecognised request type " + type ));
		}
		
		logMessage( "AZ2 reply sent: " + from_buddy.getString() + " <- " + reply );

		return( reply );
	}
		
	public chatInstance
	createChat(
		BuddyPluginBuddy[]		buddies )
	{
		byte[]	id_bytes = new byte[20];
		
		new SecureRandom().nextBytes( id_bytes );
		
		String	id = Base32.encode( id_bytes );
		
		chatInstance	chat;
		
		synchronized( chats ){

			chat = new chatInstance( id );
			
			chats.put( id, chat );
		}
		
		logMessage( "Chat " + chat.getID() + " created" );

		informCreated( chat );
					
		chat.addParticipants( buddies, true );
		
		return( chat );
	}
	
	protected void
	destroyChat(
		chatInstance	chat )
	{
		synchronized( chats ){

			chats.remove( chat.getID());
		}
		
		logMessage( "Chat " + chat.getID() + " destroyed" );
		
		informDestroyed( chat );
	}
	
	protected void
	informCreated(
		chatInstance		chat )
	{
		Iterator	it = listeners.iterator();
		
		while( it.hasNext()){
			
			((BuddyPluginAZ2Listener)it.next()).chatCreated( chat );
		}
	}
	
	protected void
	informDestroyed(
		chatInstance		chat )
	{
		Iterator	it = listeners.iterator();
		
		while( it.hasNext()){
			
			((BuddyPluginAZ2Listener)it.next()).chatDestroyed( chat );
		}
	}
	
	public void
	sendAZ2Message(
		BuddyPluginBuddy	buddy,
		String				msg )
	{
		try{
			Map	request = new HashMap();
			
			request.put( "type", new Long( RT_AZ2_REQUEST_MESSAGE ));
			request.put( "msg", msg.getBytes());
			
			sendMessage( buddy, request );
				
		}catch( Throwable e ){
			
			logMessageAndPopup( "Send message failed", e );
		}
	}
	
	protected void
	sendAZ2Chat(
		BuddyPluginBuddy	buddy,
		Map					msg )
	{
		try{
			Map	request = new HashMap();
			
			request.put( "type", new Long( RT_AZ2_REQUEST_CHAT ));
			request.put( "msg", msg );
			
			sendMessage( buddy, request );
				
		}catch( Throwable e ){
			
			logMessageAndPopup( "Send message failed", e );
		}
	}
	
	public void
	sendAZ2Torrent(
		Torrent				torrent,
		BuddyPluginBuddy	buddy )
	{
		try{
			
			Map	request = new HashMap();
			
			request.put( "type", new Long( RT_AZ2_REQUEST_SEND_TORRENT ));
			request.put( "torrent", torrent.writeToBEncodedData());
			
			sendMessage( buddy, request );
			
		}catch( Throwable e ){
			
			logMessageAndPopup( "Send torrent failed", e );
		}
	}
	
	public void
	sendAZ2TrackerMessage(
		BuddyPluginBuddy						buddy,
		Map										msg,
		final BuddyPluginAZ2TrackerListener		listener )
	{
		logMessage( "AZ2 request sent: " + buddy.getString() + " <- " + msg );

		try{
			Map	request = new HashMap();
			
			request.put( "type", new Long( RT_AZ2_REQUEST_TRACK ));
			request.put( "msg", msg );
			
			buddy.sendMessage(
				BuddyPlugin.SUBSYSTEM_AZ2,
				request,
				SEND_TIMEOUT,
				new BuddyPluginBuddyReplyListener()
				{
					public void
					replyReceived(
						BuddyPluginBuddy		from_buddy,
						Map						reply )
					{
						int type = ((Long)reply.get( "type")).intValue();
						
						if ( type != RT_AZ2_REPLY_TRACK ){
							
							sendFailed( from_buddy, new BuddyPluginException( "Mismatched reply type" ));
						}
						
						listener.messageReceived( from_buddy, (Map)reply.get( "msg" ));
					}
					
					public void
					sendFailed(
						BuddyPluginBuddy		to_buddy,
						BuddyPluginException	cause )
					{
						listener.messageFailed( to_buddy, cause );
					}
				});
				
		}catch( Throwable e ){
			
			logMessageAndPopup( "Send message failed", e );
		}
	}
	
	public void
	sendAZ2RSSMessage(
		BuddyPluginBuddy						buddy,
		Map										msg,
		final BuddyPluginAZ2TrackerListener		listener )
	{
		logMessage( "AZ2 request sent: " + buddy.getString() + " <- " + msg );

		try{
			Map	request = new HashMap();
			
			request.put( "type", new Long( RT_AZ2_REQUEST_RSS ));
			request.put( "msg", msg );
			
			buddy.sendMessage(
				BuddyPlugin.SUBSYSTEM_AZ2,
				request,
				SEND_TIMEOUT,
				new BuddyPluginBuddyReplyListener()
				{
					public void
					replyReceived(
						BuddyPluginBuddy		from_buddy,
						Map						reply )
					{
						int type = ((Long)reply.get( "type")).intValue();
						
						if ( type != RT_AZ2_REPLY_RSS ){
							
							sendFailed( from_buddy, new BuddyPluginException( "Mismatched reply type" ));
						}
						
						listener.messageReceived( from_buddy, (Map)reply.get( "msg" ));
					}
					
					public void
					sendFailed(
						BuddyPluginBuddy		to_buddy,
						BuddyPluginException	cause )
					{
						listener.messageFailed( to_buddy, cause );
					}
				});
				
		}catch( Throwable e ){
			
			logMessage( "Send message failed", e );
		}
	}
	
	protected void
	sendMessage(
		BuddyPluginBuddy	buddy,
		Map					request )
	
		throws BuddyPluginException
	{
		logMessage( "AZ2 request sent: " + buddy.getString() + " <- " + request );

		buddy.getMessageHandler().queueMessage( 
				BuddyPlugin.SUBSYSTEM_AZ2,
				request,
				SEND_TIMEOUT );		
	}
	
	public void
	addListener(
		BuddyPluginAZ2Listener		listener )
	{
		listeners.add( listener );
	}
	
	public void
	removeListener(
		BuddyPluginAZ2Listener		listener )
	{
		listeners.remove( listener );
	}
	
	public void
	addTrackerListener(
		BuddyPluginAZ2TrackerListener		listener )
	{
		track_listeners.add( listener );
	}
	
	public void
	removeTrackerListener(
		BuddyPluginAZ2TrackerListener		listener )
	{
		track_listeners.remove( listener );
	}
	
	protected void
	logMessageAndPopup(
		String		str,
		Throwable	e )
	{
		logMessageAndPopup( str + ": " + Debug.getNestedExceptionMessage(e));
	}
	
	protected void
	logMessageAndPopup(
		String		str )
	{
		logMessage( str );
		
		plugin.getPluginInterface().getUIManager().showMessageBox(
			"azbuddy.msglog.title", "!" + str + "!", UIManagerEvent.MT_OK );
	}
	
	protected void
	logMessage(
		String		str )
	{
		plugin.logMessage( str );
	}
	
	protected void
	logMessage(
		String		str,
		Throwable 	e )
	{
		plugin.logMessage( str + ": " + Debug.getNestedExceptionMessage(e));
	}
	
	public class
	chatInstance
		extends BuddyPluginAdapter
	{
		private String		id;
		
		private Map				participants 	= new HashMap();
		private CopyOnWriteList	listeners 		= new CopyOnWriteList();
		
		private List			history			= new ArrayList();
		
		protected
		chatInstance(
			String		_id )
		{
			id		= _id;
			
			plugin.addListener( this );
		}
		
		public String
		getID()
		{
			return( id );
		}
		
		public void
		buddyAdded(
			BuddyPluginBuddy	buddy )
		{
			buddyChanged( buddy );
		}
		
		public void
		buddyRemoved(
			BuddyPluginBuddy	buddy )
		{
			chatParticipant p = getParticipant( buddy );
			
			if ( p != null ){
				
				Iterator it = listeners.iterator();
				
				while( it.hasNext()){
					
					try{
						((BuddyPluginAZ2ChatListener)it.next()).participantRemoved( p );
						
					}catch( Throwable e ){
						
						Debug.printStackTrace(e);
					}
				}
			}
		}

		public void
		buddyChanged(
			BuddyPluginBuddy	buddy )
		{
			chatParticipant p = getParticipant( buddy );
			
			if ( p != null ){
			
				Iterator it = listeners.iterator();
			
				while( it.hasNext()){
					
					try{
						((BuddyPluginAZ2ChatListener)it.next()).participantChanged( p );
						
					}catch( Throwable e ){
						
						Debug.printStackTrace(e);
					}
				}
			}
		}
		
		protected void
		process(
			BuddyPluginBuddy	from_buddy,
			Map					msg )
		{
			chatParticipant p = getOrAddParticipant( from_buddy );
			
			int	type = ((Long)msg.get( "type")).intValue();
			
			if ( type == CHAT_MSG_TYPE_TEXT ){
		
				Iterator it = listeners.iterator();
				
				synchronized( history ){
				
					history.add( new chatMessage( p.getName(), msg ));
					
					if ( history.size() > 128 ){
						
						history.remove(0);
					}
				}
				
				while( it.hasNext()){
					
					try{
						((BuddyPluginAZ2ChatListener)it.next()).messageReceived( p, msg );
						
					}catch( Throwable e ){
						
						Debug.printStackTrace(e);
					}
				}
			}else if ( type == CHAT_MSG_TYPE_PARTICIPANTS_ADDED ){
				
				List added = (List)msg.get( "p" );
				
				for (int i=0;i<added.size();i++){
					
					Map	participant = (Map)added.get(i);
					
					String pk = new String((byte[])participant.get( "pk" ));
					
					if ( !pk.equals( plugin.getPublicKey())){
					
						addParticipant( pk );
					}
				}
			}
		}
		
		public void
		sendMessage(
			Map		msg )
		{
			msg.put( "type", new Long( CHAT_MSG_TYPE_TEXT ));
			
			sendMessageBase( msg );
		}
		
		protected void
		sendMessageBase(
			Map		msg )
		{
			Map	ps;
			
			synchronized( participants ){
				
				ps = new HashMap( participants );
			}
			
			msg.put( "id", id );
			
			Iterator it = ps.values().iterator();
			
			while( it.hasNext()){
				
				chatParticipant participant = (chatParticipant)it.next();
				
				if ( participant.isAuthorised()){
					
					sendAZ2Chat( participant.getBuddy(), msg );
				}
			}
		}
		
		public chatMessage[]
		getHistory()
		{
			synchronized( history ){
				
				chatMessage[]	res = new chatMessage[history.size()];
				
				history.toArray( res );
				
				return( res );
			}
		}
	
		protected chatParticipant
		getOrAddParticipant(
			BuddyPluginBuddy	buddy )
		{
			return( addParticipant( buddy ));
		}
		
		public chatParticipant
		addParticipant(
			String		pk )
		{
			chatParticipant p;
			
			BuddyPluginBuddy buddy = plugin.getBuddyFromPublicKey( pk );
			
			synchronized( participants ){
				
				p = (chatParticipant)participants.get( pk );

				if ( p != null ){
					
					return( p );
				}
				
				if ( buddy == null ){
				
					p = new chatParticipant( pk );
					
				}else{
					
					p = new chatParticipant( buddy );
				}
				
				participants.put( pk, p );
			}
			
			Iterator it = listeners.iterator();
			
			while( it.hasNext()){
				
				try{
					((BuddyPluginAZ2ChatListener)it.next()).participantAdded( p );
					
				}catch( Throwable e ){
					
					Debug.printStackTrace(e);
				}
			}
			
			return( p );
		}
		
		public chatParticipant
		addParticipant(
			BuddyPluginBuddy	buddy )
		{
			return( addParticipant( buddy.getPublicKey()));
		}
		
		public void
		addParticipants(
			BuddyPluginBuddy[]		buddies,
			boolean					inform_others )
		{
			for (int i=0;i<buddies.length;i++ ){
				
				addParticipant( buddies[i] );
			}
			
			if ( inform_others ){
			
				Map	msg = new HashMap();
				
				msg.put( "type", new Long( CHAT_MSG_TYPE_PARTICIPANTS_ADDED ));
				
				List	added = new ArrayList();
				
				msg.put( "p", added );
				
				for ( int i=0;i<buddies.length;i++){
				
					Map map = new HashMap();
					
					map.put( "pk", buddies[i].getPublicKey());
					
					added.add( map );
				}
				
				sendMessageBase( msg );
			}
		}

		protected chatParticipant
		getParticipant(
			BuddyPluginBuddy	b )
		{
			String	pk = b.getPublicKey();
			
			synchronized( participants ){
				
				chatParticipant p = (chatParticipant)participants.get( pk );

				if ( p != null ){
					
					return( p );
				}
			}
			
			return( null );
		}
		
		public chatParticipant[]
		getParticipants()
		{
			synchronized( participants ){

				chatParticipant[]	res = new chatParticipant[participants.size()];
				
				participants.values().toArray( res );
				
				return( res );
			}
		}
		
		protected void
		removeParticipant(
			chatParticipant		p )
		{
			boolean	removed;
			
			synchronized( participants ){

				removed = participants.remove( p.getPublicKey()) != null;
			}
			
			if ( removed ){
				
				Iterator it = listeners.iterator();
				
				while( it.hasNext()){
					
					try{
						((BuddyPluginAZ2ChatListener)it.next()).participantRemoved( p );
						
					}catch( Throwable e ){
						
						Debug.printStackTrace(e);
					}
				}
			}
		}
		
		public void
		destroy()
		{
			plugin.removeListener( this );

			destroyChat( this );
		}
		
		public void
		addListener(
			BuddyPluginAZ2ChatListener		listener )
		{
			listeners.add( listener );
		}
		
		public void
		removeListener(
			BuddyPluginAZ2ChatListener		listener )
		{
			listeners.remove( listener );
		}
	}
	
	public class
	chatParticipant
	{
		private BuddyPluginBuddy	buddy;
		private String				public_key;
		
		protected
		chatParticipant(
			BuddyPluginBuddy		_buddy )
		{
			buddy = _buddy;
		}
		
		protected
		chatParticipant(
			String			pk  )
		{
			public_key = pk;
		}
		
		public boolean
		isAuthorised()
		{
			return( buddy != null );
		}
		
		public BuddyPluginBuddy
		getBuddy()
		{
			return( buddy );
		}
		
		public String
		getPublicKey()
		{
			if ( buddy != null ){
				
				return( buddy.getPublicKey());
			}
			
			return( public_key );
		}
		
		public String
		getName()
		{
			if ( buddy != null ){
				
				return( buddy.getName());
			}
			
			return( public_key );
		}
	}
	
	public class
	chatMessage
	{
		private String		nick;
		private Map			map;
		
		protected
		chatMessage(
			String			_nick,
			Map				_map )
		{
			nick		= _nick;
			map			= _map;
		}
		
		public String
		getNickName()
		{
			return( nick );
		}
		
		public Map
		getMessage()
		{
			return( map );
		}
	}
}
