/*
Copyright 2002-2004 Steve Jolly

This file is part of MiniMe

MiniMe 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; specifically version 2 of the License.

MiniMe 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 MiniMe; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

import org.jibble.pircbot.*;
import java.sql.*;
import java.util.*;
import java.io.*;

public class MiniMe extends PircBot implements ReplyConstants {

	private Hashtable channelKeys=new Hashtable();
	private Vector channels=new Vector();

	public String ServerName;
	public int ServerPort;
	public String ServerPassword;
	public String BotName;

	public static String DBHost = new String("localhost");
	public static String DBName = new String("minime");
	public static String DBUserName = new String("minime");
	public static String DBPassword = new String("db_password");

	public MiniMe(String CServerName, int CServerPort, String CServerPassword, String CBotName) {
		ServerName=CServerName;
		ServerPort=CServerPort;
		ServerPassword=CServerPassword;
		BotName=CBotName;
		this.setName(BotName);
		this.setLogin(BotName);
		try {
			FileInputStream cis = new FileInputStream("channels.dat");
			ObjectInputStream	ois = new ObjectInputStream(cis);
			channels = (Vector) ois.readObject();
			channelKeys = (Hashtable) ois.readObject();
			ois.close();
			cis.close();
		} catch (Exception e) {
			//doesn't matter if we can't read in these classes, we just won't join anything :-)
		}

	}

	private void storePersistentData() {
		try {
			FileOutputStream cos = new FileOutputStream("channels.dat");
			ObjectOutputStream oos = new ObjectOutputStream(cos);
			oos.writeObject(channels);
			oos.writeObject(channelKeys);
			oos.close();
			cos.close();
		} catch (Exception e) {
			//if we can't write out the classes, hard luck :-)
		}
	}

	public void joinChannels() {
		Iterator channelIterator=channels.iterator();
		while (channelIterator.hasNext()) {
			String channel=(String)channelIterator.next();
			String key=new String();
			if (channelKeys.containsKey(channel)) key=(String)channelKeys.get(channel);	//mental namespace collision...
			joinChannel(channel, key);
		}
	}

	public void onMessage(String channel, String sender, String login, String hostname, String message) {
		IRCLine m = new IRCLine(message);
		m.checkAddressed(BotName);
		messageHandler(channel, sender, login, hostname, m);
	}

	public void onPrivateMessage(String sender, String login, String hostname, String message) {
		IRCLine m = new IRCLine(message);
		m.checkAddressed(BotName);
		m.addressed = true;	//it's a private message, right?
		messageHandler(sender, sender, login, hostname, m);
	}

	private void messageHandler(String channel, String sender, String login, String hostname, IRCLine m) {
		if ((!isListMember("ignorelist", channel, sender))||(isListMember("ownerlist", channel, sender))) {
			if (m.size()>1) {
				String targetChannel;	//select the target channel for the specified operation...
				if (m.elementAt(1).toString().equalsIgnoreCase("for")) {	//...either from the command line...
					m.remove(1);
					targetChannel=m.elementAt(1).toString();
					m.remove(1);
				} else {				//...or as the default channel
					targetChannel=channel;
				}
				
				if (validChannel(targetChannel)&&validList(m.elementAt(0).toString())&&m.size()>1) {
					if (m.elementAt(1).toString().equalsIgnoreCase("add")) {
						m.remove(1);	//delete the word "add"
						String list=m.elementAt(0).toString();
						while (m.size()>1) {
							m.remove(0);
							addListMember(list, targetChannel, m.elementAt(0).toString(), sender);
						}
					}
					else if (m.elementAt(1).toString().equalsIgnoreCase("remove")) {
						m.remove(1);
						String list=m.elementAt(0).toString();
						while (m.size()>1) {
							m.remove(0);
							removeListMember(list, targetChannel, m.elementAt(0).toString(), sender);
						}
					}
					else if (m.elementAt(1).toString().equalsIgnoreCase("list")) {
						String list=m.elementAt(0).toString();
						String stringList=showList(list, targetChannel, sender);
						if (!stringList.equals("")) sendMessage(channel, list+" for "+targetChannel+": "+stringList);
					}
				}
			}
			if (m.addressed && m.starts("join")) {
				m.remove(0);
				String TargetChannel=m.elementAt(0).toString();
				String TargetChannelKey=new String();
				if (m.size()>1) { //look for a channel key
					TargetChannelKey=m.elementAt(1).toString();
					m.remove(1);
				}
				if (validChannel(TargetChannel)) {
					if (isListMember("ownerlist",TargetChannel,sender,sender)) joinChannel(TargetChannel, TargetChannelKey);
				}
			}
			else if (m.addressed && m.starts("leave")) {
				while (m.size()>1) {
					m.remove(0);
					String targetChannel=m.elementAt(0).toString();
					if (validChannel(targetChannel)) {
						if (isListMember("ownerlist",m.elementAt(0).toString(),sender)) partChannel(m.elementAt(0).toString(), "asked to leave by "+sender);
						channels.remove(m.elementAt(0).toString());
						storePersistentData();
					}
				}
			}
			else if (m.addressed && m.starts("help")) {
				m.remove(0);
				if (m.size()==0) {
					sendMessage(channel, sender+": syntax is '"+BotName+": list [for channel] command nick. Available commands are add, remove, list, join and leave.  Available lists are trustedlist, ownerlist and ignorelist.  Try 'help command' for details");
				} else if (m.starts("add")) {
					sendMessage(channel, sender+": 'add' adds a nick to the specified list, eg '"+BotName+": trustedlist for #geldray add throat' would add 'throat' to the trustedlist for channel #geldray.");
				} else if (m.starts("remove")) {
					sendMessage(channel, sender+": 'remove' removes a nick from the specified list, eg '"+BotName+": ownerlist remove wallace' would remove 'wallace' from the ownerlist for the current channel.");
				} else if (m.starts("list")) {
					sendMessage(channel, sender+": 'list' shows all the nicks in the specified list, eg '"+BotName+": ignorelist list' would show all the nicks in the ignorelist for the current channel.");
				} else if (m.starts("join")) {
					sendMessage(channel, sender+": 'join [key]' tells minime to join the specified channel with an optional key; eg '"+BotName+": join #bot soup' would make "+BotName+"join #bot with key 'soup'.  You must be on the ownerlist for the channel in question, or telling "+BotName+" to join a new channel in order to do this.");
				} else if (m.starts("leave")) {
					sendMessage(channel, sender+": 'leave' tells minime to leave the specified channel, eg '"+BotName+": leave #icsf' would make "+BotName+"leave #icsf.  You must be on the ownerlist for the channel in question to do this.");
				} else {
					sendMessage(channel, sender+": sorry - that command is not recognised");
				}
			}
		}
	}
	
	protected void onJoin(String channel, String sender, String login, String hostname) {
		onActivity(channel, sender);
		if (sender.equals(BotName)) { //if we just joined a channel...
			sendRawLine("mode "+channel);
			if (!channels.contains(channel)) channels.add(channel);
			storePersistentData();
		}
	} 

	protected void onPart(String channel, String sender, String login, String hostname) {
		if (sender.equals(BotName)) {
			channels.remove(channel);
			storePersistentData();
		}
	}
	
	protected void onDeop(String channel, String sourceNick, String sourceLogin, String sourceHostname, String recipient) {
		onActivity(channel, recipient);
	}

	protected void onActivity(String channel, String sender) {
		if (isListMember("trustedlist",channel,sender)) {
			op(channel,sender);
		}
	}

	protected void onDisconnect () {
		int tries=0;
		while (tries<1000 && !isConnected()) {
			try {
				connect(ServerName, ServerPort, ServerPassword);
			} catch (Exception e) {
				this.log("Connection attempt "+tries+" failed.");
				tries++;
				if (tries>=1000000) System.exit(0);
			}
		} //while
//		sendAction(a channel, "poings");
	}

	protected void onKick (String channel, String kickerNick, String kickerLogin, String kickerHostname, String recipientNick, String reason) {
		if (recipientNick.equals(BotName)) {
			joinChannel(channel);
		}
	}

	protected void onSetChannelKey(String channel, String sourceNick, String sourceLogin, String sourceHostname, String key) {
		channelKeys.put(channel, key);
		storePersistentData();
	}

	protected void onRemoveChannelKey(String channel, String sourceNick, String sourceLogin, String sourceHostname, String key) {
		channelKeys.remove(channel);
		storePersistentData();
	}

	protected void onServerResponse(int code, String response) {
		if (code==RPL_CHANNELMODEIS) {
			StringTokenizer channelMode=new StringTokenizer(response, " ");
			String recipient=channelMode.nextToken();
			if (!recipient.equals(BotName)) { //the first token should be the name of the bot
				this.log("Unable to interpret channel mode line '"+response+"'.");
				return;
			}

			String channel=channelMode.nextToken();
			String modeLine=new String();
			while (channelMode.hasMoreTokens()) {
				modeLine=modeLine.concat(channelMode.nextToken()+" ");
			}			String key=extractChannelKey(modeLine);

			channelKeys.put(channel, key);
			storePersistentData();
		}
	}

	private Connection DBOpen () {
		Connection DBCon = null;
		try {
			Class.forName("org.gjt.mm.mysql.Driver").newInstance();
			DBCon = DriverManager.getConnection("jdbc:mysql://"+DBHost+"/"+DBName+"?user="+DBUserName+"&password="+DBPassword);
		} catch (Exception e) {
			this.log("Database connection attempt failed.");
		}
		return DBCon;
	}
	
	private ResultSet DBQuery (Connection DBCon, String Query) {
		ResultSet DBrs = null;
		try {
			Statement DBstmt = DBCon.createStatement();
			DBrs = DBstmt.executeQuery(Query);
		} catch (Exception e) {
			this.log("Query failed: "+Query.toString());
		}
		return DBrs;
	}

	private boolean isListMember (String list, String channel, String nick) {
		if (validList(list)) {
			int rows = getListSize(list, channel, nick);
			if (rows==0) return false;
			else return true;
		} else return false;
	}

	private boolean isListMember (String list, String channel, String nick, String requestingNick) {
		if (!validList(list)) {
			sendMessage(requestingNick, list+" is not a valid list - sorry!");
			return false;
		}
		if (!validChannel(channel)) {
			sendMessage(requestingNick, channel+" is not a valid channel - sorry!");
			return false;
		}
		int listEntries = getListSize(list, channel);
		int mentions = getListSize(list, channel, nick);
		if (list.equalsIgnoreCase("ownerlist")&&listEntries==0) {
			actuallyAddListMember("ownerlist", channel, requestingNick);
			sendMessage (requestingNick, "no ownerlist found for channel "+channel+": creating one with you on it!");
			return true;
		}
		if (mentions==0) return false;
		return true;
	}

	private void addListMember (String list, String channel, String nick, String requestingNick) {
//		this.log("called addListMember("+list+", "+channel+", "+nick+", "+requestingNick+")");
		if (validList(list)) {
			if (!isListMember("ownerlist", channel, requestingNick)) {		//you must be on the ownerlist to add nicks
				if (getListSize("ownerlist", channel)>0) {	//if you aren't, make sure there *is* an ownerlist for that channel...
					sendMessage(requestingNick, "sorry - you're not on the ownerlist for "+channel);
				} else {													//if there isn't then add the requesting nick to it!
					if (list.equals("ownerlist") && !requestingNick.equalsIgnoreCase(nick)) {	//assuming that's not what was requested...
						actuallyAddListMember ("ownerlist", channel, requestingNick);
					}
					actuallyAddListMember (list, channel, nick);
					sendMessage (requestingNick, "no ownerlist found for channel "+channel+": creating one with you on it!");
					sendMessage (requestingNick, "also adding "+nick+" to "+list+" for channel "+channel);
				}
			} else if (isListMember(list, channel, nick)) {						//no duplicates!
				sendMessage (requestingNick, nick+" is already a member of the "+list+" on "+channel+"!");
			} else {
				sendMessage (requestingNick, "adding "+nick+" to "+list+" for channel "+channel);
				actuallyAddListMember (list, channel, nick);
			}
		} else {
//			sendMessage(requestingNick, list+" is not a valid list - sorry!");
		}
	}

	private void actuallyAddListMember (String list, String channel, String nick) {
		Connection DBCon;
		DBCon = DBOpen();
		EscapedString escList = new EscapedString(list+channel);
		EscapedString escNick = new EscapedString(nick);
		ResultSet rs = DBQuery(DBCon, "insert into minime (list,nick) values ('"+escList.toString()+"', '"+escNick.toString()+"');");
	}

	private void removeListMember (String list, String channel, String nick, String requestingNick) {
		if (validList(list)) {
			if (!isListMember("ownerlist", channel, requestingNick)) {		//you must be on the ownerlist to remove nicks
				sendMessage(requestingNick, "sorry - you're not on the ownerlist for "+channel);
			} else if (!isListMember(list, channel, nick)) {					//don't try and remove things that aren't there...
				sendMessage (requestingNick, nick+" is not a member of the "+list+" on "+channel+"!");
			} else {
				sendMessage (requestingNick, "removing "+nick+" from "+list+" for channel "+channel);
				Connection DBCon;
				DBCon = DBOpen();
				EscapedString escList = new EscapedString(list+channel);
				EscapedString escNick = new EscapedString(nick);
				ResultSet rs = DBQuery(DBCon, "delete from minime where list='"+escList.toString()+"' && nick='"+escNick.toString()+"';");
			}
			if (getListSize("ownerlist", channel)==0) {
				sendMessage (requestingNick, "the ownerlist for channel "+channel+" is now empty - the trustedlist and ignorelist will also be cleared");
				clearList("trustedlist", channel);
				clearList("ignorelist", channel);
			}
		} else {
//			sendMessage(requestingNick, list+" is not a valid list - sorry!");
		}
	}
		

	private void clearList (String list, String channel) {
		Connection DBCon;
		DBCon = DBOpen();
		EscapedString escList = new EscapedString(list+channel);
		ResultSet rs = DBQuery(DBCon, "delete from minime where list='"+escList.toString()+"';");
	}

	private String showList (String list, String channel, String requestingNick) {
		if (validList(list)) {
			Connection DBCon;
			DBCon = DBOpen();
			EscapedString escList = new EscapedString(list+channel);
			ResultSet rs = DBQuery(DBCon, "select nick from minime where list='"+escList.toString()+"';");
			String stringList=new String("[");
			try {
				while (rs.next()) {
					String nextNick=rs.getString(1);
					stringList+=nextNick+", ";
				}
			} catch (SQLException e) {
				this.log("error occurred during iteration through resultset in showList ("+list+", "+channel+", "+requestingNick+")");
			}
			if (stringList.length()>1) stringList=stringList.substring(0,stringList.length()-2);
			stringList+="]";
			return stringList;
		} else {
//			sendMessage(requestingNick, list+" is not a valid list - sorry!");
			return "";
		}
	}

	private int getListSize (String list, String channel) {
		Connection DBCon;
		DBCon = DBOpen();
		EscapedString escList = new EscapedString(list+channel);
		ResultSet rs = DBQuery(DBCon, "select nick from minime where list='"+escList.toString()+"';");
		int rows=0;
		try {
			while (rs.next()) {
				rows++;
			}
		} catch (SQLException e) {
			this.log("error occurred during iteration through resultset in getListSize ("+list+", "+channel+")");
		}
		return rows;
	}

	private int getListSize (String list, String channel, String nick) {
		Connection DBCon;
		DBCon = DBOpen();
		EscapedString escList = new EscapedString(list+channel);
		EscapedString escNick = new EscapedString(nick);
		ResultSet rs = DBQuery(DBCon, "select nick from minime where list='"+escList.toString()+"' && nick='"+escNick.toString()+"';");
		int rows=0;
		try {
			while (rs.next()) {
				rows++;
			}
		} catch (SQLException e) {
			this.log("error occurred during iteration through resultset in getListSize ("+list+", "+channel+")");
		}
		return rows;
	}
	
	private boolean validList(String list) {
		if (list.equalsIgnoreCase("ownerlist")) return true;
		if (list.equalsIgnoreCase("trustedlist")) return true;
		if (list.equalsIgnoreCase("ignorelist")) return true;
		return false;
	}

	private boolean validChannel(String channel) {
		return channel.matches("(?:#|&)[^ \\a\\x00\\r\\n,]+");
	}

	private String extractChannelKey(String modeLine) {
		StringTokenizer response=new StringTokenizer(modeLine, " ");
		String key=new String();
		String modes;
		try {
			modes=response.nextToken();
		} catch (NoSuchElementException e) {
			return key;
		}
		
		for(int i=1;i<modes.length();i++) {
			String currentMode=modes.substring(i,i+1);
			if (currentMode.equals("l")) {
				try {
					String nullString=response.nextToken();
				} catch (NoSuchElementException e) {
					//this probably isn't a very good way of dealing with non RFC-compliant input data
				}
			} else if (currentMode.equals("k")) {
				try {
					key=response.nextToken();
				}	catch (NoSuchElementException e) {
					//neither is this
				}
				return key;
			}
		}
		return key;
	}
}
