/*
 * 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: ChannelLog.java 1702 2005-03-26 21:54:38Z gbevin $
 */
package com.uwyn.drone.webui.elements.pub;

import java.util.*;

import com.uwyn.drone.DroneConfig;
import com.uwyn.drone.core.Bot;
import com.uwyn.drone.core.BotsRunner;
import com.uwyn.drone.core.Channel;
import com.uwyn.drone.modules.exceptions.LogManagerException;
import com.uwyn.drone.modules.logmanagement.DatabaseLogsFactory;
import com.uwyn.drone.modules.logmanagement.LogResultProcessor;
import com.uwyn.drone.protocol.ServerMessage;
import com.uwyn.rife.config.Config;
import com.uwyn.rife.engine.Element;
import com.uwyn.rife.engine.exceptions.EngineException;
import com.uwyn.rife.site.FormBuilder;
import com.uwyn.rife.site.ValidationBuilder;
import com.uwyn.rife.site.ValidationError;
import com.uwyn.rife.template.Template;
import com.uwyn.rife.tools.ExceptionUtils;
import com.uwyn.rife.tools.Localization;
import com.uwyn.rife.tools.StringUtils;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.highlight.Highlighter;
import org.apache.lucene.search.highlight.QueryScorer;
import org.apache.lucene.search.highlight.SimpleHTMLFormatter;

public class ChannelLog extends Element
{
    public static final SimpleDateFormat	DATE_FORMAT = new SimpleDateFormat("yyyyMMdd", Localization.getLocale());
	public static final SimpleDateFormat	OUTPUT_TIME_FORMAT = new SimpleDateFormat("HH:mm", Localization.getLocale());
    public static final SimpleDateFormat	OUTPUT_DATE_FORMAT = new SimpleDateFormat("dd-MMMM-yyyy", Localization.getLocale());

	public static final String				IRC_ACTION = "\u0001ACTION";

	public static final String[] 			NICK_COLORS = new String[] {"eeeeee", "eeeecc", "cceeee", "eeccee", "ccccee", "eecccc", "cceecc"};
	public static final Pattern				URL_HIGHLIGHT = Pattern.compile("((?:http|ftp)s?://(?:%[\\p{Digit}A-Fa-f][\\p{Digit}A-Fa-f]|[\\-_\\.!~*';\\|/?:@#&=\\+$,\\p{Alnum}])+)");
	public static final Pattern				EMAIL_HIGHLIGHT = Pattern.compile("([a-zA-Z0-9][_\\-\\.\\w]*@[\\w\\.\\-]+\\.[a-zA-Z]{2,4})");

	private Highlighter 					mHighlighter = null;
	private Search.SearchBean 				mSearchBean = null;

	static
	{
		DATE_FORMAT.setTimeZone(TimeZone.getTimeZone(DroneConfig.getTimezone()));
		OUTPUT_TIME_FORMAT.setTimeZone(TimeZone.getTimeZone(DroneConfig.getTimezone()));
		OUTPUT_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone(DroneConfig.getTimezone()));
	}

	private Template	mTemplate = null;

	static Channel validateChannel(ArrayList errors, Bot bot, String channelName)
	{
		if (null == channelName)
		{
			errors.add(new ValidationError.MANDATORY("channelname"));
		}
		else
		{
			Channel channel = bot.getJoinedChannel(channelName);
			if (null == channel)
			{
				errors.add(new ValidationError.INVALID("channelname"));
			}
			else
			{
				return channel;
			}
		}

		return null;
	}
	
	public void initialize()
	{
		mTemplate = getHtmlTemplate("drone.pub.channel_log");
	}

	public final void processElement()
	{
		ArrayList errors = new ArrayList();

		Bot			bot = null;
		Channel		channel = null;
		Calendar	day = null;

		String botname = null;
		String channelname = null;
		String daystring = null;

		botname = getInput("botname");
		channelname = getInput("channelname");
		daystring = getInput("day");

		String[] parts = StringUtils.splitToArray(getPathInfo(), "/");
		try
		{
			if (null == botname &&
				parts.length > 1)
			{
				botname = URLDecoder.decode(parts[1], "ISO-8859-1");
			}
			if (null == channelname &&
				parts.length > 2)
			{
				channelname = "#"+URLDecoder.decode(parts[2], "ISO-8859-1");
			}
			if (null == daystring &&
				parts.length > 3)
			{
				daystring = URLDecoder.decode(parts[3], "ISO-8859-1");
			}
		}
		catch (UnsupportedEncodingException e)
		{
			Logger.getLogger("com.uwyn.drone").severe(ExceptionUtils.getExceptionStackTrace(e));
		}

		if (null == botname ||
			null == channelname)
		{
			exit("back_to_list");
		}

		if (!channelname.startsWith("#"))
		{
			channelname = "#"+channelname;
		}

		bot = BotList.validateBotName(errors, botname);
		if (bot != null)
		{
			channel = validateChannel(errors, bot, channelname);
		}
		
		if (null == bot ||
			null == channel)
		{
			exit("back_to_list");
		}
		
		day = Calendar.getInstance(TimeZone.getTimeZone(DroneConfig.getTimezone()));
		if (daystring != null)
		{
			try
			{
				day.setTime(DATE_FORMAT.parse(daystring));
			}
			catch (ParseException e)
			{
				errors.add(new ValidationError.INVALID("DAY"));
			}
		}

		processChannelLog(errors, bot, channel, day);
	}

	protected void processChannelLog(ArrayList errors, Bot bot, Channel channel, Calendar day)
	{
		initializeHighlighting();

		FormBuilder form_builder = mTemplate.getBeanHandler().getFormBuilder();
		form_builder.generateField(mTemplate, "q", new String[] {mSearchBean.getKeyword()}, null);
		if (!hasInputValue("botname") ||
			!hasInputValue("channelname"))
		{
			setSubmissionForm(mTemplate, "search", new String[] {"qbot", bot.getName(), "qchannel", channel.getName()});
		}

		if (errors.size() > 0)
		{
			mTemplate
				.getBeanHandler()
				.getFormBuilder()
				.getValidationBuilder()
				.generateValidationErrors(mTemplate, errors, null, null);
		}
		else
		{
			Calendar previous = Calendar.getInstance(TimeZone.getTimeZone(DroneConfig.getTimezone()));
			Calendar next = Calendar.getInstance(TimeZone.getTimeZone(DroneConfig.getTimezone()));
			previous.set(day.get(Calendar.YEAR), day.get(Calendar.MONTH), day.get(Calendar.DAY_OF_MONTH));
			previous.add(Calendar.DAY_OF_MONTH, -1);
			next.set(day.get(Calendar.YEAR), day.get(Calendar.MONTH), day.get(Calendar.DAY_OF_MONTH));
			next.add(Calendar.DAY_OF_MONTH, 1);
			setExitQuery(mTemplate, "show_previous", "/"+StringUtils.encodeUrl(bot.getName())+"/"+StringUtils.encodeUrl(channel.getName().substring(1))+"/"+ChannelLog.DATE_FORMAT.format(previous.getTime()));
			setExitQuery(mTemplate, "show_next", "/"+StringUtils.encodeUrl(bot.getName())+"/"+StringUtils.encodeUrl(channel.getName().substring(1))+"/"+ChannelLog.DATE_FORMAT.format(next.getTime()));

			mTemplate.setValue("botname", encodeHtml(bot.getName()));
			mTemplate.setValue("channelname", encodeHtml(channel.getName()));
			mTemplate.setValue("servername", encodeHtml(channel.getServer().getServerName()));
			mTemplate.setValue("day", encodeHtml(OUTPUT_DATE_FORMAT.format(day.getTime())));

			GetLogMessages log_messages = new GetLogMessages();
			try
			{
				if (DatabaseLogsFactory.get().getLogMessages(log_messages, bot, channel, day))
				{
					setOutput("botname", bot.getName());
					setOutput("channelname", channel.getName());
					setOutput("day", ChannelLog.DATE_FORMAT.format(day.getTime()));
					setExitQuery(mTemplate, "download");
				}
				else
				{
					mTemplate.setValue("download", "");
				}
			}
			catch (LogManagerException e)
			{
				throw new EngineException(e);
			}
		}

		print(mTemplate);
	}

	private void initializeHighlighting() throws EngineException
	{
		Query query = null;

		mSearchBean = (Search.SearchBean)getNamedInputBean("SearchBean");
		if (mSearchBean != null &&
			mSearchBean.getKeyword() != null &&
			!mSearchBean.getKeyword().equals(""))
		{
			try
			{
				query = QueryParser.parse(mSearchBean.getKeyword(), "message", new StandardAnalyzer());
			}
			catch (org.apache.lucene.queryParser.ParseException e)
			{
				// just ignore it, nothing I can do
			}
		}

		if (query != null)
		{
			try
			{
				Collection 	bots = BotsRunner.getRepInstance().getBots();
				Iterator 	iter = bots.iterator();

				String readerDir = null;
				while (iter.hasNext())
				{
					Bot bot = (Bot)iter.next();

					if (bot.getName().equals(getInput("botname")))
					{
						StringBuffer key = new StringBuffer();
						key
							.append(Config.getRepInstance().getString("LUCENE_DIR"))
							.append(File.separator)
							.append(bot.getName())
							.append("-")
							.append(bot.getServer().getServerName())
							.append("-")
							.append(getInput("channelname").substring(1));

						readerDir = key.toString();
					}
				}

				if (readerDir != null)
				{
					IndexReader 		reader = IndexReader.open(readerDir);
					SimpleHTMLFormatter formatter = new SimpleHTMLFormatter("<span class=\"highlighted\">", "</span>");

					mHighlighter = new Highlighter(formatter, new QueryScorer(query.rewrite(reader)));
				}
				else
				{
					throw new IOException("Couldn't find the correct index directory for this bot, channel, and server combination");
				}
			}
			catch (IOException e)
			{
				// nothing I can do, non-essential feature
				Logger
					.getLogger("com.uwyn.drone.webui.elements.pub")
					.severe(ExceptionUtils.getExceptionStackTrace(e));
			}
		}
	}

	public void doSearch()
	{
		String query = getParameter("q");

		if (query != null && !query.equals(""))
		{
			Search.SearchBean bean = new Search.SearchBean();
			bean.setKeyword(query);
			//bean.setDate(Search.INPUT_DATE_FORMAT_SHORT.format(DATE_FORMAT.parse(getInput("day"))));
			bean.setChannel(getInput("botname", getParameter("qbot"))+" - "+getInput("channelname", getParameter("qchannel")));
	
			setNamedOutputBean("SearchBean", bean);
	
			exit("search");
		}
		else
		{
			ValidationBuilder 	validationBuilder = mTemplate.getBeanHandler().getFormBuilder().getValidationBuilder();
			List 				validationErrors = new ArrayList();
			
			validationErrors.add(new ValidationError.INVALID("q"));
			
			validationBuilder.generateErrorMarkings(mTemplate, validationErrors, null, null);
			validationBuilder.generateValidationErrors(mTemplate, validationErrors, null, null);
		}
		
		processElement();
	}

	private class GetLogMessages extends LogResultProcessor
	{
		private HashMap	mNickColors = new HashMap();

		private int mColorCounter = 0;

		public boolean gotMessage(Timestamp moment, ServerMessage serverMessage)
		{
			String nickname = serverMessage.getPrefix().getNickName();

			String nickcolor = (String)mNickColors.get(nickname);
			if (null == nickcolor)
			{
				nickcolor = NICK_COLORS[mColorCounter++%NICK_COLORS.length];
				mNickColors.put(nickname, nickcolor);
			}
			mTemplate.setValue("bgcolor", nickcolor);

			mTemplate.setValue("time", OUTPUT_TIME_FORMAT.format(moment));
			mTemplate.setValue("nickname", encodeHtml(nickname));

			// translate the \u0001ACTION command which corresponds to
			// /me so that the user's nickname is used instead
			StringBuffer message = new StringBuffer();
			if (serverMessage.getPrefix() != null &&
				serverMessage.getTrailing().startsWith(IRC_ACTION))
			{
				message.append(serverMessage.getPrefix().getNickName());
				message.append(serverMessage.getTrailing().substring(IRC_ACTION.length()));
			}
			else
			{
				message.append(serverMessage.getTrailing());
			}

			String encoded_message = encodeHtml(message.toString());

			if (mHighlighter != null)
			{
				try
				{
					TokenStream tokenStream = new StandardAnalyzer().tokenStream("message", new StringReader(encoded_message));
					String highlighted = mHighlighter.getBestFragments(tokenStream, encoded_message, 25, "...");

					if (!highlighted.equals(""))
					{
						encoded_message = highlighted;
					}
				}
				catch (IOException e)
				{
					Logger
						.getLogger("com.uwyn.drone.webui.elements.pub")
						.severe(ExceptionUtils.getExceptionStackTrace(e));
				}
			}
			
			encoded_message = convertUrl(encoded_message, URL_HIGHLIGHT);

			Matcher email_matcher = EMAIL_HIGHLIGHT.matcher(encoded_message);
			encoded_message = email_matcher.replaceAll("<a href=\"mailto:$1\">$1</a>");

			mTemplate.setValue("message", encoded_message);

			mTemplate.appendBlock("messages", "message");

			return true;
		}
	}

	public static String convertUrl(String source, Pattern pattern)
	{
		String result = source;

		Matcher	url_matcher = pattern.matcher(source);
		boolean found = url_matcher.find();
        if (found)
		{
			String visual_url = null;
			String actual_url = null;
			int last = 0;
            StringBuffer sb = new StringBuffer();
			synchronized (sb)
			{
				do
				{
					actual_url = url_matcher.group(1);
					if (url_matcher.groupCount() > 1)
					{
						visual_url = url_matcher.group(2);
					}
					else
					{
						visual_url = actual_url;
					}

					sb.append(source.substring(last, url_matcher.start(0)));
					sb.append("<a href=\"");
					if (actual_url.startsWith("www."))
					{
						actual_url = "http://"+actual_url;
					}
					sb.append(actual_url);
					sb.append("\"");
					sb.append(">");
					if (visual_url.length() <= 80)
					{
						sb.append(visual_url);
					}
					else
					{
						String ellipsis = "...";
						int last_slash = visual_url.lastIndexOf("/");
						int trailing_length = visual_url.length() - last_slash + ellipsis.length();
						int start_slash = visual_url.indexOf("/", visual_url.indexOf("://")+3);
						int previous_start_slash = start_slash;
						while (start_slash+trailing_length < 80)
						{
							previous_start_slash = start_slash;
							start_slash = visual_url.indexOf("/", previous_start_slash+1);
						}

						sb.append(visual_url.substring(0, previous_start_slash+1));
						sb.append(ellipsis);
						sb.append(visual_url.substring(last_slash));
					}
					sb.append("</a>");

					last = url_matcher.end(0);

					found = url_matcher.find();
				}
				while (found);

				sb.append(source.substring(last));
				result = sb.toString();
			}
        }

		return result;
	}
}

