/*
 * Copyright 2002-2005 Uwyn bvba/sprl <info[remove] at uwyn dot com>
 * Distributed under the terms of the GNU Lesser General Public
 * License, v2.1 or later
 *
 * $Id: Seen.java 1702 2005-03-26 21:54:38Z gbevin $
 */
package com.uwyn.drone.modules;

import java.util.*;

import com.uwyn.drone.DroneConfig;
import com.uwyn.drone.core.AbstractModule;
import com.uwyn.drone.core.Bot;
import com.uwyn.drone.core.Channel;
import com.uwyn.drone.core.exceptions.CoreException;
import com.uwyn.drone.modules.exceptions.SeenManagerException;
import com.uwyn.drone.modules.seenmanagement.DatabaseSeen;
import com.uwyn.drone.modules.seenmanagement.DatabaseSeenFactory;
import com.uwyn.drone.modules.seenmanagement.SeenData;
import com.uwyn.drone.protocol.AttributeCode;
import com.uwyn.drone.protocol.ResponseCode;
import com.uwyn.drone.protocol.ServerMessage;
import com.uwyn.drone.protocol.commands.Names;
import com.uwyn.drone.protocol.commands.Privmsg;
import com.uwyn.rife.tools.ExceptionUtils;
import com.uwyn.rife.tools.StringUtils;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Seen extends AbstractModule
{
    private static final SimpleDateFormat	DATE_FORMAT = new SimpleDateFormat("EEE dd MMM yyyy HH:mm '"+DroneConfig.getTimezone()+"'");
	private static final String[]			CHANNEL_COMMANDS = new String[] {"seen"};
	private static final String[]			MESSAGE_COMMANDS = new String[] {"seen"};
	private static final ResponseCode[]		RESPONSE_CODES = new ResponseCode[] {ResponseCode.RPL_NAMREPLY, ResponseCode.RPL_ENDOFNAMES};
	private static final Pattern			SEEN_PATTERN = Pattern.compile("^\\s*(#\\w+)\\s+(.+)\\s*$");
	private static final HashMap			HELPMAP = new HashMap();
	
	static
	{
		HELPMAP.put(null,
			AttributeCode.BOLD+"Seen"+AttributeCode.BOLD+" records when someone last talked on a channel."+AttributeCode.ENDLINE+
			"It can be used to easily know when someone was active"+AttributeCode.ENDLINE+
			"on a channel for the last time."+AttributeCode.ENDLINE+
			"For more information on a specific command, type        "+AttributeCode.ENDLINE+
			AttributeCode.BOLD+"/msg $botnick help $modulename <command>"+AttributeCode.BOLD+"."+AttributeCode.ENDLINE+
			" "+AttributeCode.ENDLINE+
			AttributeCode.BOLD+"Channel commands"+AttributeCode.BOLD+""+AttributeCode.ENDLINE+
			"    !SEEN  checks when a nick was last seen"+AttributeCode.ENDLINE+
			AttributeCode.BOLD+"Privmsg commands"+AttributeCode.BOLD+""+AttributeCode.ENDLINE+
			"    SEEN   checks when a nick was last seen in a channel"+AttributeCode.ENDLINE);
		
		HELPMAP.put("!seen",
			"Syntax: "+AttributeCode.BOLD+"!SEEN <nick>"+AttributeCode.BOLD+""+AttributeCode.ENDLINE+
			" "+AttributeCode.ENDLINE+
			"Checks when someone last spoke on a channel. The time is"+AttributeCode.ENDLINE+
			"output together with what the last message was."+AttributeCode.ENDLINE+
			"If the person is currently online, this will be noticed"+AttributeCode.ENDLINE+
			"and reported accordingly."+AttributeCode.ENDLINE);
		
		HELPMAP.put("seen",
			"Syntax: "+AttributeCode.BOLD+"SEEN <#channel> <nick>"+AttributeCode.BOLD+""+AttributeCode.ENDLINE+
			" "+AttributeCode.ENDLINE+
			"Checks when someone last spoke on a channel. The time is"+AttributeCode.ENDLINE+
			"output together with what the last message was."+AttributeCode.ENDLINE+
			"If the person is currently online, this will be noticed"+AttributeCode.ENDLINE+
			"and reported accordingly."+AttributeCode.ENDLINE);
	}
	
	private	SeenQueue	mChannelSeensWaiting = new SeenQueue();
	private	SeenQueue	mMessageSeensWaiting = new SeenQueue();
	
	private	SeenQueue	mFinishedChannelNames = new SeenQueue();
	private	SeenQueue	mConstructedChannelNames = new SeenQueue();
	
	public String getName()
	{
		return "SEEN";
	}
	
	public String getDescription()
	{
		return "Stores when someone was last seen on a channel.";
	}
	
	public Map getHelpMap()
	{
		return HELPMAP;
	}
	
	public String[] getChannelCommands()
	{
		return CHANNEL_COMMANDS;
	}
	
	public String[] getMessageCommands()
	{
		return MESSAGE_COMMANDS;
	}
	
	public boolean processesChannelMessages()
	{
		return true;
	}
	
	public ResponseCode[] getResponseCodes()
	{
		return RESPONSE_CODES;
	}
	
	public void channelCommand(Bot bot, Channel channel, String nick, String command, String arguments, ServerMessage fullMessage)
	throws CoreException
	{
		if (command.equals("seen"))
		{
			String seen = arguments.trim().toLowerCase();
			
			// First drone requests the list of current channel names to
			// see of the requested nick isn't currently online.
			// After the response of the names-list, drone will determine
			// what the correct answer is
			// This is done in the response() method.
			synchronized (mChannelSeensWaiting)
			{
				// register the nick as waiting for a repsonse
				ArrayList seens_waiting = mChannelSeensWaiting.getSeens(channel);
				if (null == seens_waiting)
				{
					seens_waiting = new ArrayList();
					mChannelSeensWaiting.put(channel, seens_waiting);
				}
				if (!seens_waiting.contains(seen))
				{
					seens_waiting.add(seen);
					
					// request the current list of names in the channel
					bot.send(new Names(channel.getName()));
				}
			}
		}
	}
	
	public void messageCommand(Bot bot, String nick, String command, String arguments, ServerMessage fullMessage)
	throws CoreException
	{
		if (command.equals("seen"))
		{
			if (null == arguments ||
				0 == arguments.length())
			{
				bot.send(new Privmsg(nick, "You need to provide an argument to the command."));
				return;
			}
			
			Matcher seen_matcher = SEEN_PATTERN.matcher(arguments.toLowerCase());
			
			if (!seen_matcher.matches() ||
				seen_matcher.groupCount() != 2)
			{
				bot.send(new Privmsg(nick, "Invalid syntax '"+command+" "+arguments+"'"));
				return;
			}
			
			// obtain the requested channel
			String channel_name = seen_matcher.group(1);
			String seen = seen_matcher.group(2);
			Channel	channel = bot.getServer().getChannel(channel_name);
			if (null == channel)
			{
				return;
			}

			// First drone requests the list of current channel names to
			// see of the requested nick isn't currently online.
			// After the response of the names-list, drone will determine
			// what the correct answer is.
			// This is done in the response() method.
			synchronized (mMessageSeensWaiting)
			{
				// register the nick as waiting for a repsonse
				ArrayList seens_waiting = mMessageSeensWaiting.getSeens(channel);
				if (null == seens_waiting)
				{
					seens_waiting = new ArrayList();
					mMessageSeensWaiting.put(channel, seens_waiting);
				}
				
				int			message_seen_index = seens_waiting.indexOf(seen);
				MessageSeen	message_seen = null;
				if (-1 == message_seen_index)
				{
					message_seen = new MessageSeen(seen);
					seens_waiting.add(message_seen);
				}
				else
				{
					message_seen = (MessageSeen)seens_waiting.get(message_seen_index);
				}
				
				if (!message_seen.getNicks().contains(nick))
				{
					message_seen.getNicks().add(nick);
					
					// request the current list of names in the channel
					bot.send(new Names(channel.getName()));
				}
			}
		}
	}
	
	public void channelMessage(Bot bot, Channel channel, String nick, ServerMessage fullMessage)
	{
		SeenData		seen_data = new SeenData(nick, new Timestamp(Calendar.getInstance().getTimeInMillis()), fullMessage.getTrailing(), fullMessage.getRaw());
		DatabaseSeen	database_seen = DatabaseSeenFactory.get();
		try
		{
			database_seen.recordSeen(bot, channel, seen_data);
		}
		catch (SeenManagerException e)
		{
			Logger.getLogger("com.uwyn.drone.modules").severe(ExceptionUtils.getExceptionStackTrace(e));
		}
	}

	public void response(Bot bot, String nick, ResponseCode responseCode, ServerMessage fullMessage)
	throws CoreException
	{
		// obtain the channel the response came from
		String	channel_name = null;
		if (ResponseCode.RPL_NAMREPLY == responseCode)
		{
			channel_name = (String)fullMessage.getParameters().get(2);
		}
		if (ResponseCode.RPL_ENDOFNAMES == responseCode)
		{
			channel_name = (String)fullMessage.getParameters().get(1);
		}
		if (null == channel_name ||
			0 == channel_name.length())
		{
			return;
		}
		Channel	channel = bot.getServer().getChannel(channel_name);
		if (null == channel)
		{
			return;
		}
		
		synchronized (mConstructedChannelNames)
		{
			// received additional name entries
			if (ResponseCode.RPL_NAMREPLY == responseCode)
			{
				ArrayList nicknames = mConstructedChannelNames.getSeens(channel);
				if (null == nicknames)
				{
					nicknames = new ArrayList();
					mConstructedChannelNames.put(channel, nicknames);
				}
				ArrayList	received_names = StringUtils.split(fullMessage.getTrailing(), " ");
				Iterator	received_names_it = received_names.iterator();
				String		received_name = null;
				while (received_names_it.hasNext())
				{
					received_name = (String)received_names_it.next();
					// remove operator and voice indicators
					if (received_name.startsWith("@") ||
						received_name.startsWith("+"))
					{
						nicknames.add(received_name.substring(1));
					}
					else
					{
						nicknames.add(received_name);
					}
				}
			}
			// received all name entries, so process all the waiting seen messages
			else if (ResponseCode.RPL_ENDOFNAMES == responseCode)
			{
				ArrayList channel_nicknames = mConstructedChannelNames.removeSeens(channel);
				if (null == channel_nicknames)
				{
					return;
				}
				
				mFinishedChannelNames.put(channel, channel_nicknames);
				
				// check of there are seen nicks names that are waiting for a
				// channel namelist
				if (!mChannelSeensWaiting.hasChannel(channel) &&
					!mMessageSeensWaiting.hasChannel(channel))
				{
					return;
				}

				synchronized (mChannelSeensWaiting)
				{
					ArrayList	channel_seens = mChannelSeensWaiting.getSeens(channel);
					String 		channel_seen = null;
					
					// go over all the waiting channel seen requests and process
					// them, removing them from the waiting list
					while (channel_seens != null &&
						   channel_seens.size() > 0)
					{
						channel_seen = (String)channel_seens.remove(0);
						String reply = processSeenWaiting(bot, channel, channel_seen);
						if (reply != null)
						{
							channel.send(reply);
						}
					}
				}

				synchronized (mMessageSeensWaiting)
				{
					ArrayList	message_seens = mMessageSeensWaiting.getSeens(channel);
					MessageSeen message_seen = null;
					
					// go over all the waiting message seen requests and process
					// them, removing them from the waiting list
					while (message_seens != null &&
						   message_seens.size() > 0)
					{
						message_seen = (MessageSeen)message_seens.remove(0);
						String reply = processSeenWaiting(bot, channel, message_seen.getSeen());
						if (reply != null)
						{
							Iterator	nicks_it = message_seen.getNicks().iterator();
							while (nicks_it.hasNext())
							{
								bot.send(new Privmsg((String)nicks_it.next(), reply));
							}
						}
					}
				}
			}
		}
	}

	public String processSeenWaiting(Bot bot, Channel channel, String seen)
	{
		String reply = null;
	
		try
		{
			// query the database for information about the nickname
			DatabaseSeen	database_seen = DatabaseSeenFactory.get();
			SeenData		search_result = database_seen.getSeen(bot, channel, seen);

			DATE_FORMAT.setTimeZone(TimeZone.getTimeZone(DroneConfig.getTimezone()));

			// check if the nick is part of the current channel names
			Iterator	channel_nicknames_it = mFinishedChannelNames.getSeens(channel).iterator();
			String		channel_nickname = null;
			while (channel_nicknames_it.hasNext())
			{
				channel_nickname = (String)channel_nicknames_it.next();
				if (seen.equals(channel_nickname.toLowerCase()))
				{
					StringBuffer	formatted_result = new StringBuffer();
					formatted_result.append(channel_nickname);
					formatted_result.append(" is currently online in ");
					formatted_result.append(channel.getName());
					if (null == search_result)
					{
						formatted_result.append(", but never said anything that I saw.");
					}
					else
					{
						formatted_result.append(" and last spoke on ");
						formatted_result.append(DATE_FORMAT.format(search_result.getMoment()));
						formatted_result.append(", saying '");
						formatted_result.append(search_result.getDisplayMessage());
						formatted_result.append("'.");
					}
					return formatted_result.toString();
				}
			}
			
			if (null == search_result)
			{
				StringBuffer	formatted_result = new StringBuffer();
				formatted_result.append("I've never seen ");
				formatted_result.append(seen);
				formatted_result.append(" talk in ");
				formatted_result.append(channel.getName());
				formatted_result.append(".");
				reply =  formatted_result.toString();
				return reply;
			}
			else
			{
				ServerMessage message = ServerMessage.parse(search_result.getRaw());
				
				StringBuffer	formatted_result = new StringBuffer();
				formatted_result.append(search_result.getNickname());
				formatted_result.append(" (");
				formatted_result.append(message.getPrefix().getRaw().substring(1));
				formatted_result.append(") was last seen in ");
				formatted_result.append(channel.getName());
				formatted_result.append(" on ");
				formatted_result.append(DATE_FORMAT.format(search_result.getMoment()));
				formatted_result.append(", saying '");
				formatted_result.append(search_result.getDisplayMessage());
				formatted_result.append("'.");
				reply =  formatted_result.toString();
				return reply;
			}
		}
		catch (SeenManagerException e)
		{
			Logger.getLogger("com.uwyn.drone.modules").severe(ExceptionUtils.getExceptionStackTrace(e));
		}
		
		return reply;
	}
	
	private class MessageSeen
	{
		private String		mSeen = null;
		private ArrayList	mNicks = new ArrayList();
		
		private MessageSeen()
		{
		}
		
		public MessageSeen(String seen)
		{
			setSeen(seen);
		}
		
		public void setSeen(String seen)
		{
			mSeen = seen;
		}
		
		public String getSeen()
		{
			return mSeen;
		}
		
		public void setNicks(ArrayList nicks)
		{
			mNicks = nicks;
		}
		
		public ArrayList getNicks()
		{
			return mNicks;
		}
		
		public boolean equals(Object other)
		{
			if (other instanceof MessageSeen)
			{
				MessageSeen other_instance = (MessageSeen)other;
				if (null != other_instance &&
					other_instance.getSeen().equals(this.getSeen()))
				{
					return true;
				}
			}
			if (other instanceof String)
			{
				String other_instance = (String)other;
				if (null != other_instance &&
					other_instance.equals(this.getSeen()))
				{
					return true;
				}
			}
			
			return false;
		}
		
		public int hashCode()
		{
			return mSeen.hashCode();
		}
	}
	
	private class SeenQueue
	{
		private	HashMap	mChannelSeens = new HashMap();
		
		public void put(Channel channel, ArrayList seens)
		{
			mChannelSeens.put(channel, seens);
		}
		
		public boolean hasChannel(Channel channel)
		{
			return mChannelSeens.containsKey(channel);
		}
		
		public ArrayList getSeens(Channel channel)
		{
			return (ArrayList)mChannelSeens.get(channel);
		}
		
		public ArrayList removeSeens(Channel channel)
		{
			return (ArrayList)mChannelSeens.remove(channel);
		}
	}
}
