<commit_before>/*
 * Copyright (c) 2006-2008 <NAME>, <NAME>, <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.dmdirc.addons.logging;

import com.dmdirc.Channel;
import com.dmdirc.Main;
import com.dmdirc.Query;
import com.dmdirc.Server;
import com.dmdirc.actions.ActionManager;
import com.dmdirc.actions.CoreActionType;
import com.dmdirc.actions.interfaces.ActionType;
import com.dmdirc.commandparser.CommandManager;
import com.dmdirc.config.Identity;
import com.dmdirc.config.IdentityManager;
import com.dmdirc.config.prefs.PreferencesCategory;
import com.dmdirc.config.prefs.PreferencesManager;
import com.dmdirc.config.prefs.PreferencesSetting;
import com.dmdirc.config.prefs.PreferencesType;
import com.dmdirc.interfaces.ActionListener;
import com.dmdirc.logger.ErrorLevel;
import com.dmdirc.logger.Logger;
import com.dmdirc.parser.ChannelClientInfo;
import com.dmdirc.parser.ChannelInfo;
import com.dmdirc.parser.ClientInfo;
import com.dmdirc.parser.IRCParser;
import com.dmdirc.plugins.Plugin;
import com.dmdirc.ui.interfaces.InputWindow;
import com.dmdirc.ui.interfaces.Window;
import com.dmdirc.ui.messages.Styliser;

import java.awt.Color;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Hashtable;
import java.util.Map;
import java.util.Stack;

import java.util.Timer;
import java.util.TimerTask;

/**
 * Adds logging facility to client.
 *
 * @author Shane 'Dataforce' McCormack
 * @version $Id: LoggingPlugin.java 969 2007-04-30 18:38:20Z ShaneMcC $
 */
public final class LoggingPlugin extends Plugin implements ActionListener {

	/** What domain do we store all settings in the global config under. */
	private static final String MY_DOMAIN = "plugin-Logging";

	/** The command we registered. */
	private LoggingCommand command;

	/** Open File */
	private class OpenFile {
		public long lastUsedTime = System.currentTimeMillis();
		public BufferedWriter writer = null;
		public OpenFile(final BufferedWriter writer) {
			this.writer = writer;
		}
	}

	/** Timer used to close idle files */
	private Timer idleFileTimer;

	/** Hashtable of open files. */
	private final Map<String, OpenFile> openFiles = new Hashtable<String, OpenFile>();

	/** Date format used for "File Opened At" log. */
	final DateFormat openedAtFormat = new SimpleDateFormat("EEEE MMMM dd, yyyy - HH:mm:ss");
	
	/**
	 * Creates a new instance of the Logging Plugin.
	 */
	public LoggingPlugin() { super(); }

	/**
	 * Called when the plugin is loaded.
	 */
	@Override
	public void onLoad() {
		// Set defaults
		final Identity defaults = IdentityManager.getAddonIdentity();
		defaults.setOption(MY_DOMAIN, "general.directory", Main.getConfigDir() + "logs" + System.getProperty("file.separator"));
		defaults.setOption(MY_DOMAIN, "general.networkfolders", "true");
		defaults.setOption(MY_DOMAIN, "advanced.filenamehash", "false");
		defaults.setOption(MY_DOMAIN, "general.addtime", "true");
		defaults.setOption(MY_DOMAIN, "general.timestamp", "[dd/MM/yyyy HH:mm:ss]");
		defaults.setOption(MY_DOMAIN, "general.stripcodes", "true");
		defaults.setOption(MY_DOMAIN, "general.channelmodeprefix", "true");
		defaults.setOption(MY_DOMAIN, "backbuffer.autobackbuffer", "true");
		defaults.setOption(MY_DOMAIN, "backbuffer.lines", "10");
		defaults.setOption(MY_DOMAIN, "backbuffer.colour", "14");
		defaults.setOption(MY_DOMAIN, "backbuffer.timestamp", "false");
		defaults.setOption(MY_DOMAIN, "history.lines", "50000");
		defaults.setOption(MY_DOMAIN, "advanced.usedate", "false");
		defaults.setOption(MY_DOMAIN, "advanced.usedateformat", "yyyy/MMMM");

		final File dir = new File(IdentityManager.getGlobalConfig().getOption(MY_DOMAIN, "general.directory"));
		if (dir.exists()) {
			if (!dir.isDirectory()) {
				Logger.userError(ErrorLevel.LOW, "Unable to create logging dir (file exists instead)");
			}
		} else {
			if (!dir.mkdirs()) {
				Logger.userError(ErrorLevel.LOW, "Unable to create logging dir");
			}
		}

		command = new LoggingCommand();
		ActionManager.addListener(this,
		                 CoreActionType.CHANNEL_OPENED,
		                 CoreActionType.CHANNEL_CLOSED,
		                 CoreActionType.CHANNEL_MESSAGE,
		                 CoreActionType.CHANNEL_SELF_MESSAGE,
		                 CoreActionType.CHANNEL_ACTION,
		                 CoreActionType.CHANNEL_SELF_ACTION,
		                 CoreActionType.CHANNEL_GOTTOPIC,
		                 CoreActionType.CHANNEL_TOPICCHANGE,
		                 CoreActionType.CHANNEL_JOIN,
		                 CoreActionType.CHANNEL_PART,
		                 CoreActionType.CHANNEL_QUIT,
		                 CoreActionType.CHANNEL_KICK,
		                 CoreActionType.CHANNEL_NICKCHANGE,
		                 CoreActionType.CHANNEL_MODECHANGE,
		                 CoreActionType.QUERY_OPENED,
		                 CoreActionType.QUERY_CLOSED,
		                 CoreActionType.QUERY_MESSAGE,
		                 CoreActionType.QUERY_SELF_MESSAGE,
		                 CoreActionType.QUERY_ACTION,
		                 CoreActionType.QUERY_SELF_ACTION);
		
		// Close idle files every hour.
		idleFileTimer = new Timer("LoggingPlugin Timer");
		idleFileTimer.schedule(new TimerTask(){
			public void run() {
				timerTask();
			}
		}, 3600000);
	}
	
	/**
	 * What to do every hour when the timer fires.
	 */
	private void timerTask() {
		// Oldest time to allow
		final long oldestTime = System.currentTimeMillis() - 3480000;
		
		synchronized (openFiles) {
			for (String filename : (new Hashtable<String, OpenFile>(openFiles)).keySet()) {
				OpenFile file = openFiles.get(filename);
				if (file.lastUsedTime < oldestTime) {
					try {
						file.writer.close();
						openFiles.remove(filename);
					} catch (IOException e) {
						Logger.userError(ErrorLevel.LOW, "Unable to close idle file (File: "+filename+")");
					}
				}
			}
		}
	}

	/**
	 * Called when this plugin is unloaded.
	 */
	@Override
	public void onUnload() {
		idleFileTimer.cancel();
		idleFileTimer.purge();
		
		CommandManager.unregisterCommand(command);
		ActionManager.removeListener(this);
		
		synchronized (openFiles) {
			for (String filename : openFiles.keySet()) {
				OpenFile file = openFiles.get(filename);
				try {
					file.writer.close();
				} catch (IOException e) {
					Logger.userError(ErrorLevel.LOW, "Unable to close file (File: "+filename+")");
				}
			}
			openFiles.clear();
		}
	}

	/** {@inheritDoc} */
	@Override
	public void showConfig(final PreferencesManager manager) {
		final PreferencesCategory general = new PreferencesCategory("Logging", "General configuration for Logging plugin.");
		final PreferencesCategory backbuffer = new PreferencesCategory("Back Buffer", "Options related to the automatic backbuffer");
		final PreferencesCategory advanced = new PreferencesCategory("Advanced", "Advanced configuration for Logging plugin. You shouldn't need to edit this unless you know what you are doing.");
		
		general.addSetting(new PreferencesSetting(PreferencesType.TEXT, MY_DOMAIN, "general.directory", "false", "Directory", "Directory for log files"));
		general.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "general.networkfolders", "", "Separate logs by network", "Should the files be stored in a sub-dir with the networks name?"));
		general.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "general.addtime", "false", "Timestamp logs", "Should a timestamp be added to the log files?"));
		general.addSetting(new PreferencesSetting(PreferencesType.TEXT, MY_DOMAIN, "general.timestamp", "", "Timestamp format", "The String to pass to 'SimpleDateFormat' to format the timestamp"));
		general.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "general.stripcodes", "false", "Strip Control Codes", "Remove known irc control codes from lines before saving?"));
		general.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "general.channelmodeprefix", "", "Show channel mode prefix", "Show the @,+ etc next to nicknames"));
		
		backbuffer.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "backbuffer.autobackbuffer", "", "Automatically display", "Automatically display the backbuffer when a channel is joined"));
		backbuffer.addSetting(new PreferencesSetting(PreferencesType.COLOUR, MY_DOMAIN, "backbuffer.colour", "0", "Colour to use for display", "Colour used when displaying the backbuffer"));
		backbuffer.addSetting(new PreferencesSetting(PreferencesType.INTEGER, MY_DOMAIN, "backbuffer.lines", "0", "Number of lines to show", "Number of lines used when displaying backbuffer"));
		backbuffer.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "backbuffer.timestamp", "false", "Show Formatter-Timestamp", "Should the line be added to the frame with the timestamp from the formatter aswell as the file contents"));

		advanced.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "advanced.filenamehash", "false", "Add Filename hash", "Add the MD5 hash of the channel/client name to the filename. (This is used to allow channels with similar names (ie a _ not a  -) to be logged separately)"));

		advanced.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "advanced.usedate", "false", "Use Date directories", "Should the log files be in separate directories based on the date?"));
		advanced.addSetting(new PreferencesSetting(PreferencesType.TEXT, MY_DOMAIN, "advanced.usedateformat", "yyyy/MMMM", "Archive format", "The String to pass to 'SimpleDateFormat' to format the directory name(s) for archiving"));

		general.addSubCategory(backbuffer.setInline());
		general.addSubCategory(advanced.setInline());
		manager.getCategory("Plugins").addSubCategory(general.setInlineAfter());
	}

	/**
	 * Get the name of the domain we store all settings in the global config under.
	 *
	 * @return the plugins domain
	 */
	protected static String getDomain() { return MY_DOMAIN; }

	/**
	 * Log a query-related event
	 *
	 * @param type The type of the event to process
	 * @param format Format of messages that are about to be sent. (May be null)
	 * @param arguments The arguments for the event
	 */
	private void handleQueryEvent(final CoreActionType type, final StringBuffer format, final Object... arguments) {
		final Query query = (Query)arguments[0];
		if (query.getServer() == null) {
			Logger.appError(ErrorLevel.MEDIUM, "Query object has no server ("+type.toString()+")", new Exception("Query object has no server ("+type.toString()+")"));
			return;
		}
		
		final IRCParser parser = query.getServer().getParser();
		ClientInfo client;
		
		if (parser == null) {
			// Without a parser object, we might not be able to find the file to log this to.
			if (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "general.networkfolders")) {
				// We *wont* be able to, so rather than logging to an incorrect file we just won't log.
				return;
			}
			client = null;
		} else {
			client = parser.getClientInfo(query.getHost());
			if (client == null) {
				client = new ClientInfo(parser, query.getHost()).setFake(true);
			}
		}
		
		final String filename = getLogFile(client);
		
		switch (type) {
			case QUERY_OPENED:
				if (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "backbuffer.autobackbuffer")) {
					showBackBuffer(query.getFrame(), filename);
				}

				appendLine(filename, "*** Query opened at: %s", openedAtFormat.format(new Date()));
				appendLine(filename, "*** Query with User: %s", query.getHost());
				appendLine(filename, "");
				break;
			case QUERY_CLOSED:
				if (openFiles.containsKey(filename)) {
					appendLine(filename, "*** Query closed at: %s", openedAtFormat.format(new Date()));
					final BufferedWriter file = openFiles.get(filename).writer;
					try {
						file.close();
					} catch (IOException e) {
						Logger.userError(ErrorLevel.LOW, "Unable to close file (Filename: "+filename+")");
					}
					openFiles.remove(filename);
				}
				break;
			case QUERY_MESSAGE:
			case QUERY_SELF_MESSAGE:
			case QUERY_ACTION:
			case QUERY_SELF_ACTION:
				final boolean isME = (type == CoreActionType.QUERY_SELF_MESSAGE || type == CoreActionType.QUERY_SELF_ACTION);
				final String overrideNick = (isME) ? getDisplayName(parser.getMyself()) : "";
				
				if (type == CoreActionType.QUERY_MESSAGE || type == CoreActionType.QUERY_SELF_MESSAGE) {
					appendLine(filename, "<%s> %s", getDisplayName(client, overrideNick), (String)arguments[1]);
				} else {
					appendLine(filename, "* %s %s", getDisplayName(client, overrideNick), (String)arguments[1]);
				}
				break;
		}
	}
	
	/**
	 * Log a channel-related event
	 *
	 * @param type The type of the event to process
	 * @param format Format of messages that are about to be sent. (May be null)
	 * @param arguments The arguments for the event
	 */
	private void handleChannelEvent(final CoreActionType type, final StringBuffer format, final Object... arguments) {
		final Channel chan = ((Channel)arguments[0]);
		final ChannelInfo channel = chan.getChannelInfo();
		final String filename = getLogFile(channel);
		
		final ChannelClientInfo channelClient = (arguments.length > 1 && arguments[1] instanceof ChannelClientInfo) ? (ChannelClientInfo)arguments[1] : null;
		final ClientInfo client = (channelClient != null) ? channelClient.getClient() : null;

		final String message = (arguments.length > 2 && arguments[2] instanceof String) ? (String)arguments[2] : null;
		
		switch (type) {
			case CHANNEL_OPENED:
				if (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "backbuffer.autobackbuffer")) {
					showBackBuffer(chan.getFrame(), filename);
				}

				appendLine(filename, "*** Channel opened at: %s", openedAtFormat.format(new Date()));
				appendLine(filename, "");
				break;
			case CHANNEL_CLOSED:
				if (openFiles.containsKey(filename)) {
					appendLine(filename, "*** Channel closed at: %s", openedAtFormat.format(new Date()));
					final BufferedWriter file = openFiles.get(filename).writer;
					try {
						file.close();
					} catch (IOException e) {
						Logger.userError(ErrorLevel.LOW, "Unable to close file (Filename: "+filename+")");
					}
					openFiles.remove(filename);
				}
				break;
			case CHANNEL_MESSAGE:
			case CHANNEL_SELF_MESSAGE:
			case CHANNEL_ACTION:
			case CHANNEL_SELF_ACTION:
				if (type == CoreActionType.CHANNEL_MESSAGE || type == CoreActionType.CHANNEL_SELF_MESSAGE) {
					appendLine(filename, "<%s> %s", getDisplayName(client), message);
				} else {
					appendLine(filename, "* %s %s", getDisplayName(client), message);
				}
				break;
			case CHANNEL_GOTTOPIC:
				// ActionManager.processEvent(CoreActionType.CHANNEL_GOTTOPIC, this);
				final DateFormat timeFormat = new SimpleDateFormat("HH:mm:ss");
				final DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
				
				appendLine(filename, "*** Topic is: %s", channel.getTopic());
				appendLine(filename, "*** Set at: %s on %s by %s", timeFormat.format(1000 * channel.getTopicTime()), dateFormat.format(1000 * channel.getTopicTime()), channel.getTopicUser());
				break;
			case CHANNEL_TOPICCHANGE:
				appendLine(filename, "*** %s Changed the topic to: %s", getDisplayName(channelClient), message);
				break;
			case CHANNEL_JOIN:
				appendLine(filename, "*** %s (%s) joined the channel", getDisplayName(channelClient), client.toString());
				break;
			case CHANNEL_PART:
				if (message.isEmpty()) {
					appendLine(filename, "*** %s (%s) left the channel", getDisplayName(channelClient), client.toString());
				} else {
					appendLine(filename, "*** %s (%s) left the channel (%s)", getDisplayName(channelClient), client.toString(), message);
				}
				break;
			case CHANNEL_QUIT:
				if (message.isEmpty()) {
					appendLine(filename, "*** %s (%s) Quit IRC", getDisplayName(channelClient), client.toString());
				} else {
					appendLine(filename, "*** %s (%s) Quit IRC (%s)", getDisplayName(channelClient), client.toString(), message);
				}
				break;
			case CHANNEL_KICK:
				final String kickReason = (String)arguments[3];
				final ChannelClientInfo kickedClient = (ChannelClientInfo)arguments[2];
				
				if (kickReason.isEmpty()) {
					appendLine(filename, "*** %s was kicked by %s", getDisplayName(kickedClient), getDisplayName(channelClient));
				} else {
					appendLine(filename, "*** %s was kicked by %s (%s)", getDisplayName(kickedClient), getDisplayName(channelClient), kickReason);
				}
				break;
			case CHANNEL_NICKCHANGE:
				appendLine(filename, "*** %s is now %s", getDisplayName(channelClient, message), getDisplayName(channelClient));
				break;
			case CHANNEL_MODECHANGE:
				if (channelClient.getNickname().isEmpty()) {
					appendLine(filename, "*** Channel modes are: %s", message);
				} else {
					appendLine(filename, "*** %s set modes: %s", getDisplayName(channelClient), message);
				}
				break;
		}
	}

	/**
	 * Process an event of the specified type.
	 *
	 * @param type The type of the event to process
	 * @param format Format of messages that are about to be sent. (May be null)
	 * @param arguments The arguments for the event
	 */
	@Override
	public void processEvent(final ActionType type, final StringBuffer format, final Object ... arguments) {
		if (type instanceof CoreActionType) {
			final CoreActionType thisType = (CoreActionType) type;

			switch (thisType) {
				case CHANNEL_OPENED:
				case CHANNEL_CLOSED:
				case CHANNEL_MESSAGE:
				case CHANNEL_SELF_MESSAGE:
				case CHANNEL_ACTION:
				case CHANNEL_SELF_ACTION:
				case CHANNEL_GOTTOPIC:
				case CHANNEL_TOPICCHANGE:
				case CHANNEL_JOIN:
				case CHANNEL_PART:
				case CHANNEL_QUIT:
				case CHANNEL_KICK:
				case CHANNEL_NICKCHANGE:
				case CHANNEL_MODECHANGE:
					handleChannelEvent(thisType, format, arguments);
					break;
				case QUERY_OPENED:
				case QUERY_CLOSED:
				case QUERY_MESSAGE:
				case QUERY_SELF_MESSAGE:
				case QUERY_ACTION:
				case QUERY_SELF_ACTION:
					handleQueryEvent(thisType, format, arguments);
					break;
				default:
					break;
			}
		}
	}

	/**
	 * Add a backbuffer to a frame.
	 *
	 * @param frame The frame to add the backbuffer lines to
	 * @param filename File to get backbuffer from
	 */
	private void showBackBuffer(final Window frame, final String filename) {
		final int numLines = IdentityManager.getGlobalConfig().getOptionInt(MY_DOMAIN, "backbuffer.lines", 0);
		final String colour = IdentityManager.getGlobalConfig().getOption(MY_DOMAIN, "backbuffer.colour");
		final boolean showTimestamp = IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "backbuffer.timestamp");
		if (frame == null) {
			Logger.userError(ErrorLevel.LOW, "Given a null frame");
			return;
		}

		final File testFile = new File(filename);
		if (testFile.exists()) {
			try {
				final ReverseFileReader file = new ReverseFileReader(testFile);
				// Because the file includes a newline char at the end, an empty line
				// is returned by getLines. To counter this, we call getLines(1) and do
				// nothing with the output.
				file.getLines(1);
				final Stack<String> lines = file.getLines(numLines);
				while (!lines.empty()) {
					frame.addLine(getColouredString(colour,lines.pop()), showTimestamp);
				}
				file.close();
				frame.addLine(getColouredString(colour,"--- End of backbuffer\n"), showTimestamp);
			} catch (FileNotFoundException e) {
				Logger.userError(ErrorLevel.LOW, "Unable to show backbuffer (Filename: "+filename+"): " + e.getMessage());
			} catch (IOException e) {
				Logger.userError(ErrorLevel.LOW, "Unable to show backbuffer (Filename: "+filename+"): " + e.getMessage());
			} catch (SecurityException e) {
				Logger.userError(ErrorLevel.LOW, "Unable to show backbuffer (Filename: "+filename+"): " + e.getMessage());
			}
		}
	}

	/**
	 * Get a coloured String.
	 * If colour is invalid, IRC Colour 14 will be used.
	 *
	 * @param colour The colour the string should be (IRC Colour or 6-digit hex colour)
	 * @param line the line to colour
	 * @return The given line with the appropriate irc codes appended/prepended to colour it.
	 */
	private String getColouredString(final String colour, final String line) {
		String res = null;
		if (colour.length() < 3) {
			int num;

			try {
				num = Integer.parseInt(colour);
			} catch (NumberFormatException ex) {
				num = -1;
			}

			if (num >= 0 && num <= 15) {
				res = String.format("%c%02d%s%1$c", Styliser.CODE_COLOUR, num, line);
			}
		} else if (colour.length() == 6) {
			try {
				Color.decode("#" + colour);
				res = String.format("%c%s%s%1$c", Styliser.CODE_HEXCOLOUR, colour, line);
			} catch (NumberFormatException ex) { /* Do Nothing */ }
		}

		if (res == null) {
			res = String.format("%c%02d%s%1$c", Styliser.CODE_COLOUR, 14, line);
		}
		return res;
	}

	/**
	 * Add a line to a file.
	 *
	 * @param filename Name of file to write to
	 * @param format Format of line to add. (NewLine will be added Automatically)
	 * @param args Arguments for format
	 * @return true on success, else false.
	 */
	private boolean appendLine(final String filename, final String format, final Object... args) {
		return appendLine(filename, String.format(format, args));
	}

	/**
	 * Add a line to a file.
	 *
	 * @param filename Name of file to write to
	 * @param line Line to add. (NewLine will be added Automatically)
	 * @return true on success, else false.
	 */
	private boolean appendLine(final String filename, final String line) {
		final StringBuffer finalLine = new StringBuffer();
		
		if (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "general.addtime")) {
			final DateFormat dateFormat = new SimpleDateFormat(IdentityManager.getGlobalConfig().getOption(MY_DOMAIN, "general.timestamp"));
			finalLine.append(dateFormat.format(new Date()));
		}
		
		if (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "general.stripcodes")) {
			finalLine.append(Styliser.stipControlCodes(line));
		} else {
			finalLine.append(line);
		}
		//System.out.println("[Adding] "+filename+" => "+finalLine);
		BufferedWriter out = null;
		try {
			if (openFiles.containsKey(filename)) {
				OpenFile of = openFiles.get(filename);
				of.lastUsedTime = System.currentTimeMillis();
				out = of.writer;
			} else {
				out = new BufferedWriter(new FileWriter(filename, true));
				openFiles.put(filename, new OpenFile(out));
			}
			out.write(finalLine.toString());
			out.newLine();
			out.flush();
			return true;
		} catch (IOException e) {
			/*
			 * Do Nothing
			 *
			 * Makes no sense to keep adding errors to the logger when we can't
			 * write to the file, as chances are it will happen on every incomming
			 * line.
			 */
		}
		return false;
	}

	/**
	 * Get the name of the log file for a specific object.
	 *
	 * @param obj Object to get name for
	 * @return the name of the log file to use for this object.
	 */
	private String getLogFile(final Object obj) {
		final StringBuffer directory = new StringBuffer();
		final StringBuffer file = new StringBuffer();
		String md5String = "";
	
		directory.append(IdentityManager.getGlobalConfig().getOption(MY_DOMAIN, "general.directory"));
		if (directory.charAt(directory.length()-1) != File.separatorChar) {
			directory.append(File.separatorChar);
		}
		
		if (obj == null) {
			file.append("null.log");
		} else if (obj instanceof ChannelInfo) {
			final ChannelInfo channel = (ChannelInfo) obj;
			if (channel.getParser() != null) {
				addNetworkDir(directory, file, channel.getParser().getNetworkName());
			}
			file.append(sanitise(channel.getName().toLowerCase()));
			md5String = channel.getName();
		} else if (obj instanceof ClientInfo) {
			final ClientInfo client = (ClientInfo) obj;
			if (client.getParser() != null) {
				addNetworkDir(directory, file, client.getParser().getNetworkName());
			}
			file.append(sanitise(client.getNickname().toLowerCase()));
			md5String = client.getNickname();
		} else {
			file.append(sanitise(obj.toString().toLowerCase()));
			md5String = obj.toString();
		}
		
		if (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "advanced.usedate")) {
			final String dateFormat = IdentityManager.getGlobalConfig().getOption(MY_DOMAIN, "advanced.usedateformat");
			final String dateDir = (new SimpleDateFormat(dateFormat)).format(new Date());
			directory.append(dateDir);
			if (directory.charAt(directory.length()-1) != File.separatorChar) {
				directory.append(File.separatorChar);
			}
			
			if (!new File(directory.toString()).exists() && !(new File(directory.toString())).mkdirs()) {
				Logger.userError(ErrorLevel.LOW, "Unable to create date dirs");
			}
		}
		
		if (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "advanced.filenamehash")) {
			file.append('.');
			file.append(md5(md5String));
		}
		file.append(".log");
		
		return directory.toString() + file.toString();
	}
	
	/**
	 * This function adds the networkName to the log file.
	 * It first tries to create a directory for each network, if that fails
	 * it will prepend the networkName to the filename instead.
	 *
	 * @param directory Current directory name
	 * @param file Current file name
	 * @param networkName Name of network
	 */
	private void addNetworkDir(final StringBuffer directory, final StringBuffer file, final String networkName) {
		if (!IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "general.networkfolders")) {
			return;
		}
	
		final String network = sanitise(networkName.toLowerCase());
		
		boolean prependNetwork = false;
		
		// Check dir exists
		final File dir = new File(directory.toString()+network+System.getProperty("file.separator"));
		if (dir.exists() && !dir.isDirectory()) {
			Logger.userError(ErrorLevel.LOW, "Unable to create networkfolders dir (file exists instead)");
			// Prepend network name to file instead.
			prependNetwork = true;
		} else if (!dir.exists() && !dir.mkdirs()) {
			Logger.userError(ErrorLevel.LOW, "Unable to create networkfolders dir");
			prependNetwork = true;
		}
		
		if (prependNetwork) {
			file.insert(0, " -- ");
			file.insert(0, network);
		} else {
			directory.append(network);
			directory.append(System.getProperty("file.separator"));
		}
	}

	/**
	 * Sanitise a string to be used as a filename.
	 *
	 * @param name String to sanitise
	 * @return Sanitised version of name that can be used as a filename.
	 */
	private String sanitise(final String name) {
		// Replace illegal chars with
		return name.replaceAll("[^\\w\\.\\s\\-\\#\\&\\_]", "_");
	}

	/**
	 * Get the md5 hash of a string.
	 *
	 * @param string String to hash
	 * @return md5 hash of given string
	 */
	private String md5(final String string) {
		try {
			final MessageDigest m = MessageDigest.getInstance("MD5");
			m.update(string.getBytes(), 0, string.length());
			return new BigInteger(1, m.digest()).toString(16);
		} catch (NoSuchAlgorithmException e) {
			return "";
		}
	}
	
	/**
	 * Get name to display for client.
	 *
	 * @param client The client to get the display name for
	 * @return name to display
	 */
	private String getDisplayName(final ClientInfo client) {
		return getDisplayName(client, "");
	}

	/**
	 * Get name to display for client.
	 *
	 * @param client The client to get the display name for
	 * @param overrideNick Nickname to display instead of real nickname
	 * @return name to display
	 */
	private String getDisplayName(final ClientInfo client, final String overrideNick) {
		if (overrideNick.isEmpty()) {
			return (client == null) ? "Unknown Client" : client.getNickname() ;
		} else {
			return overrideNick;
		}
	}

	/**
	 * Get name to display for channelClient (Taking into account the channelmodeprefix setting).
	 *
	 * @param channelClient The client to get the display name for
	 * @return name to display
	 */
	private String getDisplayName(final ChannelClientInfo channelClient) {
		return getDisplayName(channelClient, "");
	}

	/**
	 * Get name to display for channelClient (Taking into account the channelmodeprefix setting).
	 *
	 * @param channelClient The client to get the display name for
	 * @param overrideNick Nickname to display instead of real nickname
	 * @return name to display
	 */
	private String getDisplayName(final ChannelClientInfo channelClient, final String overrideNick) {
		final boolean addModePrefix = (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "general.channelmodeprefix"));
		
		if (channelClient == null) {
			return (overrideNick.isEmpty()) ? "Unknown Client" : overrideNick;
		} else if (overrideNick.isEmpty()) {
			return (addModePrefix) ? channelClient.toString() : channelClient.getNickname();
		} else {
			return (addModePrefix) ? channelClient.getImportantModePrefix() + overrideNick : overrideNick;
		}
	}

	/**
	 * Shows the history window for the specified target, if available.
	 *
	 * @param target The window whose history we're trying to open
	 * @return True if the history is available, false otherwise
	 */<commit_msg>LoggingPlugin is no longer final for no good reason. Does not impact functionality.<commit_after>/*
 * Copyright (c) 2006-2008 <NAME>, <NAME>, <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.dmdirc.addons.logging;

import com.dmdirc.Channel;
import com.dmdirc.Main;
import com.dmdirc.Query;
import com.dmdirc.Server;
import com.dmdirc.actions.ActionManager;
import com.dmdirc.actions.CoreActionType;
import com.dmdirc.actions.interfaces.ActionType;
import com.dmdirc.commandparser.CommandManager;
import com.dmdirc.config.Identity;
import com.dmdirc.config.IdentityManager;
import com.dmdirc.config.prefs.PreferencesCategory;
import com.dmdirc.config.prefs.PreferencesManager;
import com.dmdirc.config.prefs.PreferencesSetting;
import com.dmdirc.config.prefs.PreferencesType;
import com.dmdirc.interfaces.ActionListener;
import com.dmdirc.logger.ErrorLevel;
import com.dmdirc.logger.Logger;
import com.dmdirc.parser.ChannelClientInfo;
import com.dmdirc.parser.ChannelInfo;
import com.dmdirc.parser.ClientInfo;
import com.dmdirc.parser.IRCParser;
import com.dmdirc.plugins.Plugin;
import com.dmdirc.ui.interfaces.InputWindow;
import com.dmdirc.ui.interfaces.Window;
import com.dmdirc.ui.messages.Styliser;

import java.awt.Color;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Hashtable;
import java.util.Map;
import java.util.Stack;

import java.util.Timer;
import java.util.TimerTask;

/**
 * Adds logging facility to client.
 *
 * @author Shane 'Dataforce' McCormack
 * @version $Id: LoggingPlugin.java 969 2007-04-30 18:38:20Z ShaneMcC $
 */
public class LoggingPlugin extends Plugin implements ActionListener {

	/** What domain do we store all settings in the global config under. */
	private static final String MY_DOMAIN = "plugin-Logging";

	/** The command we registered. */
	private LoggingCommand command;

	/** Open File */
	protected class OpenFile {
		public long lastUsedTime = System.currentTimeMillis();
		public BufferedWriter writer = null;
		public OpenFile(final BufferedWriter writer) {
			this.writer = writer;
		}
	}

	/** Timer used to close idle files */
	protected Timer idleFileTimer;

	/** Hashtable of open files. */
	protected final Map<String, OpenFile> openFiles = new Hashtable<String, OpenFile>();

	/** Date format used for "File Opened At" log. */
	final DateFormat openedAtFormat = new SimpleDateFormat("EEEE MMMM dd, yyyy - HH:mm:ss");
	
	/**
	 * Creates a new instance of the Logging Plugin.
	 */
	public LoggingPlugin() { super(); }

	/**
	 * Called when the plugin is loaded.
	 */
	@Override
	public void onLoad() {
		// Set defaults
		final Identity defaults = IdentityManager.getAddonIdentity();
		defaults.setOption(MY_DOMAIN, "general.directory", Main.getConfigDir() + "logs" + System.getProperty("file.separator"));
		defaults.setOption(MY_DOMAIN, "general.networkfolders", "true");
		defaults.setOption(MY_DOMAIN, "advanced.filenamehash", "false");
		defaults.setOption(MY_DOMAIN, "general.addtime", "true");
		defaults.setOption(MY_DOMAIN, "general.timestamp", "[dd/MM/yyyy HH:mm:ss]");
		defaults.setOption(MY_DOMAIN, "general.stripcodes", "true");
		defaults.setOption(MY_DOMAIN, "general.channelmodeprefix", "true");
		defaults.setOption(MY_DOMAIN, "backbuffer.autobackbuffer", "true");
		defaults.setOption(MY_DOMAIN, "backbuffer.lines", "10");
		defaults.setOption(MY_DOMAIN, "backbuffer.colour", "14");
		defaults.setOption(MY_DOMAIN, "backbuffer.timestamp", "false");
		defaults.setOption(MY_DOMAIN, "history.lines", "50000");
		defaults.setOption(MY_DOMAIN, "advanced.usedate", "false");
		defaults.setOption(MY_DOMAIN, "advanced.usedateformat", "yyyy/MMMM");

		final File dir = new File(IdentityManager.getGlobalConfig().getOption(MY_DOMAIN, "general.directory"));
		if (dir.exists()) {
			if (!dir.isDirectory()) {
				Logger.userError(ErrorLevel.LOW, "Unable to create logging dir (file exists instead)");
			}
		} else {
			if (!dir.mkdirs()) {
				Logger.userError(ErrorLevel.LOW, "Unable to create logging dir");
			}
		}

		command = new LoggingCommand();
		ActionManager.addListener(this,
		                 CoreActionType.CHANNEL_OPENED,
		                 CoreActionType.CHANNEL_CLOSED,
		                 CoreActionType.CHANNEL_MESSAGE,
		                 CoreActionType.CHANNEL_SELF_MESSAGE,
		                 CoreActionType.CHANNEL_ACTION,
		                 CoreActionType.CHANNEL_SELF_ACTION,
		                 CoreActionType.CHANNEL_GOTTOPIC,
		                 CoreActionType.CHANNEL_TOPICCHANGE,
		                 CoreActionType.CHANNEL_JOIN,
		                 CoreActionType.CHANNEL_PART,
		                 CoreActionType.CHANNEL_QUIT,
		                 CoreActionType.CHANNEL_KICK,
		                 CoreActionType.CHANNEL_NICKCHANGE,
		                 CoreActionType.CHANNEL_MODECHANGE,
		                 CoreActionType.QUERY_OPENED,
		                 CoreActionType.QUERY_CLOSED,
		                 CoreActionType.QUERY_MESSAGE,
		                 CoreActionType.QUERY_SELF_MESSAGE,
		                 CoreActionType.QUERY_ACTION,
		                 CoreActionType.QUERY_SELF_ACTION);
		
		// Close idle files every hour.
		idleFileTimer = new Timer("LoggingPlugin Timer");
		idleFileTimer.schedule(new TimerTask(){
			public void run() {
				timerTask();
			}
		}, 3600000);
	}
	
	/**
	 * What to do every hour when the timer fires.
	 */
	protected void timerTask() {
		// Oldest time to allow
		final long oldestTime = System.currentTimeMillis() - 3480000;
		
		synchronized (openFiles) {
			for (String filename : (new Hashtable<String, OpenFile>(openFiles)).keySet()) {
				OpenFile file = openFiles.get(filename);
				if (file.lastUsedTime < oldestTime) {
					try {
						file.writer.close();
						openFiles.remove(filename);
					} catch (IOException e) {
						Logger.userError(ErrorLevel.LOW, "Unable to close idle file (File: "+filename+")");
					}
				}
			}
		}
	}

	/**
	 * Called when this plugin is unloaded.
	 */
	@Override
	public void onUnload() {
		idleFileTimer.cancel();
		idleFileTimer.purge();
		
		CommandManager.unregisterCommand(command);
		ActionManager.removeListener(this);
		
		synchronized (openFiles) {
			for (String filename : openFiles.keySet()) {
				OpenFile file = openFiles.get(filename);
				try {
					file.writer.close();
				} catch (IOException e) {
					Logger.userError(ErrorLevel.LOW, "Unable to close file (File: "+filename+")");
				}
			}
			openFiles.clear();
		}
	}

	/** {@inheritDoc} */
	@Override
	public void showConfig(final PreferencesManager manager) {
		final PreferencesCategory general = new PreferencesCategory("Logging", "General configuration for Logging plugin.");
		final PreferencesCategory backbuffer = new PreferencesCategory("Back Buffer", "Options related to the automatic backbuffer");
		final PreferencesCategory advanced = new PreferencesCategory("Advanced", "Advanced configuration for Logging plugin. You shouldn't need to edit this unless you know what you are doing.");
		
		general.addSetting(new PreferencesSetting(PreferencesType.TEXT, MY_DOMAIN, "general.directory", "false", "Directory", "Directory for log files"));
		general.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "general.networkfolders", "", "Separate logs by network", "Should the files be stored in a sub-dir with the networks name?"));
		general.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "general.addtime", "false", "Timestamp logs", "Should a timestamp be added to the log files?"));
		general.addSetting(new PreferencesSetting(PreferencesType.TEXT, MY_DOMAIN, "general.timestamp", "", "Timestamp format", "The String to pass to 'SimpleDateFormat' to format the timestamp"));
		general.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "general.stripcodes", "false", "Strip Control Codes", "Remove known irc control codes from lines before saving?"));
		general.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "general.channelmodeprefix", "", "Show channel mode prefix", "Show the @,+ etc next to nicknames"));
		
		backbuffer.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "backbuffer.autobackbuffer", "", "Automatically display", "Automatically display the backbuffer when a channel is joined"));
		backbuffer.addSetting(new PreferencesSetting(PreferencesType.COLOUR, MY_DOMAIN, "backbuffer.colour", "0", "Colour to use for display", "Colour used when displaying the backbuffer"));
		backbuffer.addSetting(new PreferencesSetting(PreferencesType.INTEGER, MY_DOMAIN, "backbuffer.lines", "0", "Number of lines to show", "Number of lines used when displaying backbuffer"));
		backbuffer.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "backbuffer.timestamp", "false", "Show Formatter-Timestamp", "Should the line be added to the frame with the timestamp from the formatter aswell as the file contents"));

		advanced.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "advanced.filenamehash", "false", "Add Filename hash", "Add the MD5 hash of the channel/client name to the filename. (This is used to allow channels with similar names (ie a _ not a  -) to be logged separately)"));

		advanced.addSetting(new PreferencesSetting(PreferencesType.BOOLEAN, MY_DOMAIN, "advanced.usedate", "false", "Use Date directories", "Should the log files be in separate directories based on the date?"));
		advanced.addSetting(new PreferencesSetting(PreferencesType.TEXT, MY_DOMAIN, "advanced.usedateformat", "yyyy/MMMM", "Archive format", "The String to pass to 'SimpleDateFormat' to format the directory name(s) for archiving"));

		general.addSubCategory(backbuffer.setInline());
		general.addSubCategory(advanced.setInline());
		manager.getCategory("Plugins").addSubCategory(general.setInlineAfter());
	}

	/**
	 * Get the name of the domain we store all settings in the global config under.
	 *
	 * @return the plugins domain
	 */
	protected static String getDomain() { return MY_DOMAIN; }

	/**
	 * Log a query-related event
	 *
	 * @param type The type of the event to process
	 * @param format Format of messages that are about to be sent. (May be null)
	 * @param arguments The arguments for the event
	 */
	protected void handleQueryEvent(final CoreActionType type, final StringBuffer format, final Object... arguments) {
		final Query query = (Query)arguments[0];
		if (query.getServer() == null) {
			Logger.appError(ErrorLevel.MEDIUM, "Query object has no server ("+type.toString()+")", new Exception("Query object has no server ("+type.toString()+")"));
			return;
		}
		
		final IRCParser parser = query.getServer().getParser();
		ClientInfo client;
		
		if (parser == null) {
			// Without a parser object, we might not be able to find the file to log this to.
			if (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "general.networkfolders")) {
				// We *wont* be able to, so rather than logging to an incorrect file we just won't log.
				return;
			}
			client = null;
		} else {
			client = parser.getClientInfo(query.getHost());
			if (client == null) {
				client = new ClientInfo(parser, query.getHost()).setFake(true);
			}
		}
		
		final String filename = getLogFile(client);
		
		switch (type) {
			case QUERY_OPENED:
				if (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "backbuffer.autobackbuffer")) {
					showBackBuffer(query.getFrame(), filename);
				}

				appendLine(filename, "*** Query opened at: %s", openedAtFormat.format(new Date()));
				appendLine(filename, "*** Query with User: %s", query.getHost());
				appendLine(filename, "");
				break;
			case QUERY_CLOSED:
				if (openFiles.containsKey(filename)) {
					appendLine(filename, "*** Query closed at: %s", openedAtFormat.format(new Date()));
					final BufferedWriter file = openFiles.get(filename).writer;
					try {
						file.close();
					} catch (IOException e) {
						Logger.userError(ErrorLevel.LOW, "Unable to close file (Filename: "+filename+")");
					}
					openFiles.remove(filename);
				}
				break;
			case QUERY_MESSAGE:
			case QUERY_SELF_MESSAGE:
			case QUERY_ACTION:
			case QUERY_SELF_ACTION:
				final boolean isME = (type == CoreActionType.QUERY_SELF_MESSAGE || type == CoreActionType.QUERY_SELF_ACTION);
				final String overrideNick = (isME) ? getDisplayName(parser.getMyself()) : "";
				
				if (type == CoreActionType.QUERY_MESSAGE || type == CoreActionType.QUERY_SELF_MESSAGE) {
					appendLine(filename, "<%s> %s", getDisplayName(client, overrideNick), (String)arguments[1]);
				} else {
					appendLine(filename, "* %s %s", getDisplayName(client, overrideNick), (String)arguments[1]);
				}
				break;
		}
	}
	
	/**
	 * Log a channel-related event
	 *
	 * @param type The type of the event to process
	 * @param format Format of messages that are about to be sent. (May be null)
	 * @param arguments The arguments for the event
	 */
	protected void handleChannelEvent(final CoreActionType type, final StringBuffer format, final Object... arguments) {
		final Channel chan = ((Channel)arguments[0]);
		final ChannelInfo channel = chan.getChannelInfo();
		final String filename = getLogFile(channel);
		
		final ChannelClientInfo channelClient = (arguments.length > 1 && arguments[1] instanceof ChannelClientInfo) ? (ChannelClientInfo)arguments[1] : null;
		final ClientInfo client = (channelClient != null) ? channelClient.getClient() : null;

		final String message = (arguments.length > 2 && arguments[2] instanceof String) ? (String)arguments[2] : null;
		
		switch (type) {
			case CHANNEL_OPENED:
				if (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "backbuffer.autobackbuffer")) {
					showBackBuffer(chan.getFrame(), filename);
				}

				appendLine(filename, "*** Channel opened at: %s", openedAtFormat.format(new Date()));
				appendLine(filename, "");
				break;
			case CHANNEL_CLOSED:
				if (openFiles.containsKey(filename)) {
					appendLine(filename, "*** Channel closed at: %s", openedAtFormat.format(new Date()));
					final BufferedWriter file = openFiles.get(filename).writer;
					try {
						file.close();
					} catch (IOException e) {
						Logger.userError(ErrorLevel.LOW, "Unable to close file (Filename: "+filename+")");
					}
					openFiles.remove(filename);
				}
				break;
			case CHANNEL_MESSAGE:
			case CHANNEL_SELF_MESSAGE:
			case CHANNEL_ACTION:
			case CHANNEL_SELF_ACTION:
				if (type == CoreActionType.CHANNEL_MESSAGE || type == CoreActionType.CHANNEL_SELF_MESSAGE) {
					appendLine(filename, "<%s> %s", getDisplayName(client), message);
				} else {
					appendLine(filename, "* %s %s", getDisplayName(client), message);
				}
				break;
			case CHANNEL_GOTTOPIC:
				// ActionManager.processEvent(CoreActionType.CHANNEL_GOTTOPIC, this);
				final DateFormat timeFormat = new SimpleDateFormat("HH:mm:ss");
				final DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
				
				appendLine(filename, "*** Topic is: %s", channel.getTopic());
				appendLine(filename, "*** Set at: %s on %s by %s", timeFormat.format(1000 * channel.getTopicTime()), dateFormat.format(1000 * channel.getTopicTime()), channel.getTopicUser());
				break;
			case CHANNEL_TOPICCHANGE:
				appendLine(filename, "*** %s Changed the topic to: %s", getDisplayName(channelClient), message);
				break;
			case CHANNEL_JOIN:
				appendLine(filename, "*** %s (%s) joined the channel", getDisplayName(channelClient), client.toString());
				break;
			case CHANNEL_PART:
				if (message.isEmpty()) {
					appendLine(filename, "*** %s (%s) left the channel", getDisplayName(channelClient), client.toString());
				} else {
					appendLine(filename, "*** %s (%s) left the channel (%s)", getDisplayName(channelClient), client.toString(), message);
				}
				break;
			case CHANNEL_QUIT:
				if (message.isEmpty()) {
					appendLine(filename, "*** %s (%s) Quit IRC", getDisplayName(channelClient), client.toString());
				} else {
					appendLine(filename, "*** %s (%s) Quit IRC (%s)", getDisplayName(channelClient), client.toString(), message);
				}
				break;
			case CHANNEL_KICK:
				final String kickReason = (String)arguments[3];
				final ChannelClientInfo kickedClient = (ChannelClientInfo)arguments[2];
				
				if (kickReason.isEmpty()) {
					appendLine(filename, "*** %s was kicked by %s", getDisplayName(kickedClient), getDisplayName(channelClient));
				} else {
					appendLine(filename, "*** %s was kicked by %s (%s)", getDisplayName(kickedClient), getDisplayName(channelClient), kickReason);
				}
				break;
			case CHANNEL_NICKCHANGE:
				appendLine(filename, "*** %s is now %s", getDisplayName(channelClient, message), getDisplayName(channelClient));
				break;
			case CHANNEL_MODECHANGE:
				if (channelClient.getNickname().isEmpty()) {
					appendLine(filename, "*** Channel modes are: %s", message);
				} else {
					appendLine(filename, "*** %s set modes: %s", getDisplayName(channelClient), message);
				}
				break;
		}
	}

	/**
	 * Process an event of the specified type.
	 *
	 * @param type The type of the event to process
	 * @param format Format of messages that are about to be sent. (May be null)
	 * @param arguments The arguments for the event
	 */
	@Override
	public void processEvent(final ActionType type, final StringBuffer format, final Object ... arguments) {
		if (type instanceof CoreActionType) {
			final CoreActionType thisType = (CoreActionType) type;

			switch (thisType) {
				case CHANNEL_OPENED:
				case CHANNEL_CLOSED:
				case CHANNEL_MESSAGE:
				case CHANNEL_SELF_MESSAGE:
				case CHANNEL_ACTION:
				case CHANNEL_SELF_ACTION:
				case CHANNEL_GOTTOPIC:
				case CHANNEL_TOPICCHANGE:
				case CHANNEL_JOIN:
				case CHANNEL_PART:
				case CHANNEL_QUIT:
				case CHANNEL_KICK:
				case CHANNEL_NICKCHANGE:
				case CHANNEL_MODECHANGE:
					handleChannelEvent(thisType, format, arguments);
					break;
				case QUERY_OPENED:
				case QUERY_CLOSED:
				case QUERY_MESSAGE:
				case QUERY_SELF_MESSAGE:
				case QUERY_ACTION:
				case QUERY_SELF_ACTION:
					handleQueryEvent(thisType, format, arguments);
					break;
				default:
					break;
			}
		}
	}

	/**
	 * Add a backbuffer to a frame.
	 *
	 * @param frame The frame to add the backbuffer lines to
	 * @param filename File to get backbuffer from
	 */
	protected void showBackBuffer(final Window frame, final String filename) {
		final int numLines = IdentityManager.getGlobalConfig().getOptionInt(MY_DOMAIN, "backbuffer.lines", 0);
		final String colour = IdentityManager.getGlobalConfig().getOption(MY_DOMAIN, "backbuffer.colour");
		final boolean showTimestamp = IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "backbuffer.timestamp");
		if (frame == null) {
			Logger.userError(ErrorLevel.LOW, "Given a null frame");
			return;
		}

		final File testFile = new File(filename);
		if (testFile.exists()) {
			try {
				final ReverseFileReader file = new ReverseFileReader(testFile);
				// Because the file includes a newline char at the end, an empty line
				// is returned by getLines. To counter this, we call getLines(1) and do
				// nothing with the output.
				file.getLines(1);
				final Stack<String> lines = file.getLines(numLines);
				while (!lines.empty()) {
					frame.addLine(getColouredString(colour,lines.pop()), showTimestamp);
				}
				file.close();
				frame.addLine(getColouredString(colour,"--- End of backbuffer\n"), showTimestamp);
			} catch (FileNotFoundException e) {
				Logger.userError(ErrorLevel.LOW, "Unable to show backbuffer (Filename: "+filename+"): " + e.getMessage());
			} catch (IOException e) {
				Logger.userError(ErrorLevel.LOW, "Unable to show backbuffer (Filename: "+filename+"): " + e.getMessage());
			} catch (SecurityException e) {
				Logger.userError(ErrorLevel.LOW, "Unable to show backbuffer (Filename: "+filename+"): " + e.getMessage());
			}
		}
	}

	/**
	 * Get a coloured String.
	 * If colour is invalid, IRC Colour 14 will be used.
	 *
	 * @param colour The colour the string should be (IRC Colour or 6-digit hex colour)
	 * @param line the line to colour
	 * @return The given line with the appropriate irc codes appended/prepended to colour it.
	 */
	protected String getColouredString(final String colour, final String line) {
		String res = null;
		if (colour.length() < 3) {
			int num;

			try {
				num = Integer.parseInt(colour);
			} catch (NumberFormatException ex) {
				num = -1;
			}

			if (num >= 0 && num <= 15) {
				res = String.format("%c%02d%s%1$c", Styliser.CODE_COLOUR, num, line);
			}
		} else if (colour.length() == 6) {
			try {
				Color.decode("#" + colour);
				res = String.format("%c%s%s%1$c", Styliser.CODE_HEXCOLOUR, colour, line);
			} catch (NumberFormatException ex) { /* Do Nothing */ }
		}

		if (res == null) {
			res = String.format("%c%02d%s%1$c", Styliser.CODE_COLOUR, 14, line);
		}
		return res;
	}

	/**
	 * Add a line to a file.
	 *
	 * @param filename Name of file to write to
	 * @param format Format of line to add. (NewLine will be added Automatically)
	 * @param args Arguments for format
	 * @return true on success, else false.
	 */
	protected boolean appendLine(final String filename, final String format, final Object... args) {
		return appendLine(filename, String.format(format, args));
	}

	/**
	 * Add a line to a file.
	 *
	 * @param filename Name of file to write to
	 * @param line Line to add. (NewLine will be added Automatically)
	 * @return true on success, else false.
	 */
	protected boolean appendLine(final String filename, final String line) {
		final StringBuffer finalLine = new StringBuffer();
		
		if (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "general.addtime")) {
			final DateFormat dateFormat = new SimpleDateFormat(IdentityManager.getGlobalConfig().getOption(MY_DOMAIN, "general.timestamp"));
			finalLine.append(dateFormat.format(new Date()));
		}
		
		if (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "general.stripcodes")) {
			finalLine.append(Styliser.stipControlCodes(line));
		} else {
			finalLine.append(line);
		}
		//System.out.println("[Adding] "+filename+" => "+finalLine);
		BufferedWriter out = null;
		try {
			if (openFiles.containsKey(filename)) {
				OpenFile of = openFiles.get(filename);
				of.lastUsedTime = System.currentTimeMillis();
				out = of.writer;
			} else {
				out = new BufferedWriter(new FileWriter(filename, true));
				openFiles.put(filename, new OpenFile(out));
			}
			out.write(finalLine.toString());
			out.newLine();
			out.flush();
			return true;
		} catch (IOException e) {
			/*
			 * Do Nothing
			 *
			 * Makes no sense to keep adding errors to the logger when we can't
			 * write to the file, as chances are it will happen on every incomming
			 * line.
			 */
		}
		return false;
	}

	/**
	 * Get the name of the log file for a specific object.
	 *
	 * @param obj Object to get name for
	 * @return the name of the log file to use for this object.
	 */
	protected String getLogFile(final Object obj) {
		final StringBuffer directory = new StringBuffer();
		final StringBuffer file = new StringBuffer();
		String md5String = "";
	
		directory.append(IdentityManager.getGlobalConfig().getOption(MY_DOMAIN, "general.directory"));
		if (directory.charAt(directory.length()-1) != File.separatorChar) {
			directory.append(File.separatorChar);
		}
		
		if (obj == null) {
			file.append("null.log");
		} else if (obj instanceof ChannelInfo) {
			final ChannelInfo channel = (ChannelInfo) obj;
			if (channel.getParser() != null) {
				addNetworkDir(directory, file, channel.getParser().getNetworkName());
			}
			file.append(sanitise(channel.getName().toLowerCase()));
			md5String = channel.getName();
		} else if (obj instanceof ClientInfo) {
			final ClientInfo client = (ClientInfo) obj;
			if (client.getParser() != null) {
				addNetworkDir(directory, file, client.getParser().getNetworkName());
			}
			file.append(sanitise(client.getNickname().toLowerCase()));
			md5String = client.getNickname();
		} else {
			file.append(sanitise(obj.toString().toLowerCase()));
			md5String = obj.toString();
		}
		
		if (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "advanced.usedate")) {
			final String dateFormat = IdentityManager.getGlobalConfig().getOption(MY_DOMAIN, "advanced.usedateformat");
			final String dateDir = (new SimpleDateFormat(dateFormat)).format(new Date());
			directory.append(dateDir);
			if (directory.charAt(directory.length()-1) != File.separatorChar) {
				directory.append(File.separatorChar);
			}
			
			if (!new File(directory.toString()).exists() && !(new File(directory.toString())).mkdirs()) {
				Logger.userError(ErrorLevel.LOW, "Unable to create date dirs");
			}
		}
		
		if (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "advanced.filenamehash")) {
			file.append('.');
			file.append(md5(md5String));
		}
		file.append(".log");
		
		return directory.toString() + file.toString();
	}
	
	/**
	 * This function adds the networkName to the log file.
	 * It first tries to create a directory for each network, if that fails
	 * it will prepend the networkName to the filename instead.
	 *
	 * @param directory Current directory name
	 * @param file Current file name
	 * @param networkName Name of network
	 */
	protected void addNetworkDir(final StringBuffer directory, final StringBuffer file, final String networkName) {
		if (!IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "general.networkfolders")) {
			return;
		}
	
		final String network = sanitise(networkName.toLowerCase());
		
		boolean prependNetwork = false;
		
		// Check dir exists
		final File dir = new File(directory.toString()+network+System.getProperty("file.separator"));
		if (dir.exists() && !dir.isDirectory()) {
			Logger.userError(ErrorLevel.LOW, "Unable to create networkfolders dir (file exists instead)");
			// Prepend network name to file instead.
			prependNetwork = true;
		} else if (!dir.exists() && !dir.mkdirs()) {
			Logger.userError(ErrorLevel.LOW, "Unable to create networkfolders dir");
			prependNetwork = true;
		}
		
		if (prependNetwork) {
			file.insert(0, " -- ");
			file.insert(0, network);
		} else {
			directory.append(network);
			directory.append(System.getProperty("file.separator"));
		}
	}

	/**
	 * Sanitise a string to be used as a filename.
	 *
	 * @param name String to sanitise
	 * @return Sanitised version of name that can be used as a filename.
	 */
	protected String sanitise(final String name) {
		// Replace illegal chars with
		return name.replaceAll("[^\\w\\.\\s\\-\\#\\&\\_]", "_");
	}

	/**
	 * Get the md5 hash of a string.
	 *
	 * @param string String to hash
	 * @return md5 hash of given string
	 */
	protected String md5(final String string) {
		try {
			final MessageDigest m = MessageDigest.getInstance("MD5");
			m.update(string.getBytes(), 0, string.length());
			return new BigInteger(1, m.digest()).toString(16);
		} catch (NoSuchAlgorithmException e) {
			return "";
		}
	}
	
	/**
	 * Get name to display for client.
	 *
	 * @param client The client to get the display name for
	 * @return name to display
	 */
	protected String getDisplayName(final ClientInfo client) {
		return getDisplayName(client, "");
	}

	/**
	 * Get name to display for client.
	 *
	 * @param client The client to get the display name for
	 * @param overrideNick Nickname to display instead of real nickname
	 * @return name to display
	 */
	protected String getDisplayName(final ClientInfo client, final String overrideNick) {
		if (overrideNick.isEmpty()) {
			return (client == null) ? "Unknown Client" : client.getNickname() ;
		} else {
			return overrideNick;
		}
	}

	/**
	 * Get name to display for channelClient (Taking into account the channelmodeprefix setting).
	 *
	 * @param channelClient The client to get the display name for
	 * @return name to display
	 */
	protected String getDisplayName(final ChannelClientInfo channelClient) {
		return getDisplayName(channelClient, "");
	}

	/**
	 * Get name to display for channelClient (Taking into account the channelmodeprefix setting).
	 *
	 * @param channelClient The client to get the display name for
	 * @param overrideNick Nickname to display instead of real nickname
	 * @return name to display
	 */
	protected String getDisplayName(final ChannelClientInfo channelClient, final String overrideNick) {
		final boolean addModePrefix = (IdentityManager.getGlobalConfig().getOptionBool(MY_DOMAIN, "general.channelmodeprefix"));
		
		if (channelClient == null) {
			return (overrideNick.isEmpty()) ? "Unknown Client" : overrideNick;
		} else if (overrideNick.isEmpty()) {
			return (addModePrefix) ? channelClient.toString() : channelClient.getNickname();
		} else {
			return (addModePrefix) ? channelClient.getImportantModePrefix() + overrideNick : overrideNick;
		}
	}

	/**
	 * Shows the history window for the specified target, if available.
	 *
	 * @param target The window whose history we're trying to open
	 * @return True if the history is available, false otherwise
	 */
{'fraction_non_alphanumeric': 0.11768984098647889, 'fraction_numerical': 0.0036364838507058086, 'mean_word_length': 4.034032284905974, 'pattern_counts': {'":': 0, '<': 27, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2005864', 'n_tokens_mistral': 20269, 'n_tokens_neox': 17486, 'n_words': 5065}