package org.jonlin;

import java.util.Date;
import java.util.Vector;
import java.util.Hashtable;
import java.util.Random;

import java.text.SimpleDateFormat;

import java.io.IOException;
import java.io.File;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;

import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;

/**
 * This is Copyrighted under GPL. Do whatever you want with it,
 * but don't blame me if your shit gets fucked. If you want to
 * complain, I'll probably ignore you, if you want to tell me about
 * a bug in my program, I may or may not fix it (probably will
 * eventually) and thank you for it later.
 *
 * jonlin@tesuji.org
 */
public class TokyoToshokanIRCBot implements Runnable
{
	Thread runThread;
	Socket ircSock;
	BufferedReader in;
	OutputStream out;

	String myChan;
	String myNick;
	String myPong;
	String myUser;
	String ircServer;
	int ircPort;
	String rssBot;
	String rssBotIRCName;         // atm useless, seems the RSS bot changes it's IRS hashed address or unidentd'd name
	String downloaderProgram;
	String downloaderFlags;
	String matchPatternsFile;
	String torrentDirectory;
	int localPort;
	boolean onlyAnime;
	String emailAddress;
	String mailRelay;
	Vector rizonPool;
	boolean notLocalhost;

	Vector recentDownloads;
	HTTPListener httpListener;
	Vector matchPatterns;
	SimpleDateFormat dateFormat;
	Vector recentReleases;

	boolean netsplit;
	long netsplitTimer;

	boolean debug;

	/**
	 * Set default values of runtime vars, read command line params and replace
	 * default values with the ones passed in as parameters.
	 */
	public TokyoToshokanIRCBot(String[] args)
	{
		runThread = new Thread(this);
		ircServer = "irc.rizon.net";
		ircPort = 6666;
		myNick = "lurker00";
		myUser = "lurker buzzy buzzy.tesuji.org :Lurker";
		myPong = "buzzy.tesuji.org";
		myChan = "#tokyotosho";
		rssBot = "TT-RSS";
		//		rssBotIRCName = "~Python@Rizon-A0264FC6.resnet.nmt.edu";
		//		rssBotIRCName = "~Python@tokyotosho.rss.bot.made.with.python";          // current but who the fuck knows?
		downloaderProgram = "wget";
		downloaderFlags = "-O {file}";
		torrentDirectory = "/home/ftp/torrents/";
		matchPatternsFile = "./patterns.txt";
		matchPatterns = new Vector();
		localPort = -1;
		notLocalhost = false;
		dateFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss ZZZZZ");
		onlyAnime = true;
		recentDownloads = new Vector();
		recentReleases = new Vector();
		rizonPool = null;

		netsplit = false;
		netsplitTimer = 60000;

		debug = false;


		try {
			for(int x=0;x<args.length;x++) {
				if(args[x].equals("-nick")) {
					myNick = args[++x];
				}
				else if(args[x].equals("-user")) {
					myUser = args[++x];
				}
				else if(args[x].equals("-pong")) {
					myPong = args[++x];
				}
				else if(args[x].equals("-channel")) {
					myChan = args[++x];
				}
				else if(args[x].equals("-server")) {
					ircServer = args[++x];
				}
				else if(args[x].equals("-port")) {
					try {
						ircPort = Integer.parseInt(args[++x]);
					}
					catch(Exception e) {
						System.err.println("Cannot understand port: "+args[x]);
						System.exit(-1);
					}
				}
				else if(args[x].equals("-rss")) {
					rssBot = args[++x];
				}
				else if(args[x].equals("-rssname")) {
					rssBotIRCName = args[++x];
				}
				else if(args[x].equals("-downloader")) {
					downloaderProgram = args[++x];
				}
				else if(args[x].equals("-flags")) {
					downloaderFlags = args[++x];
				}
				else if(args[x].equals("-patterns")) {
					matchPatternsFile = args[++x];
				}
				else if(args[x].equals("-listen")) {
					try {
						localPort = Integer.parseInt(args[++x]);
					}
					catch(Exception e) {
						System.err.println("Cannot understand port: "+args[x]);
						System.exit(-1);
					}
				}
				else if(args[x].equals("-listenall")) {
					notLocalhost = true;
				}
				else if(args[x].equals("-torrents")) {
					torrentDirectory = args[++x];
				}
				else if(args[x].equals("-h") || args[x].equals("-help")) {
					printHelp();
					System.exit(0);
				}
				else if(args[x].equals("-all")) {
					onlyAnime = false;
				}
				else if(args[x].equals("-email")) {
					emailAddress = args[++x];
				}
				else if(args[x].equals("-relay")) {
					mailRelay = args[++x];
				}
				else if(args[x].equals("-debug")) {
					debug = true;
				}
				else {
					System.err.println("Could not understand parameter: "+args[x]);
					printHelp();
					System.exit(-3);
				}
			}
		}
		catch(ArrayIndexOutOfBoundsException e) {
			System.err.println("Could not parse parameters");
			printHelp();
			System.exit(-2);
		}

		// For rizon server rotation when netsplit occurs
		if(ircServer.equalsIgnoreCase("irc.rizon.net")) {
			rizonPool = new Vector();
			rizonPool.addElement("76.76.9.134");
			rizonPool.addElement("66.7.192.11");
			rizonPool.addElement("89.46.32.179");
			rizonPool.addElement("72.11.142.40");
			rizonPool.addElement("204.8.34.130");
			rizonPool.addElement("69.64.58.164");
			rizonPool.addElement("206.53.51.113");
			rizonPool.addElement("64.18.131.116");
			rizonPool.addElement("208.185.81.185");
			rizonPool.addElement("66.111.37.204");
			rizonPool.addElement("65.23.158.11");
			// need to jumble them up a bit
			Random r = new Random();
			int iter = Math.abs(r.nextInt()%20);
			for(int x=0;x<iter;x++) {
				int index = Math.abs(r.nextInt()%(rizonPool.size()-1));
				String tmp = (String)rizonPool.elementAt(index);
				rizonPool.removeElementAt(index);
				rizonPool.addElement(tmp);
			}

			ircServer = (String)rizonPool.elementAt(rizonPool.size()-1);
		}

		loadPatterns();
		if(!connect()) {
			System.err.println("Could not connect to host: "+ircServer+":"+ircPort);
			System.exit(-5);
		}
		runThread.start();
		if(localPort>0) {
			httpListener = new HTTPListener();
		}
	}

	/**
	 * Main loop that reads input from the IRC server, handles server PINGs
	 * handles checked for netplits and reconnecting, and handles the parsing
	 * of the release info.
	 */
	public void run()
	{
		String line = null;
		String privmsg = " PRIVMSG "+myChan+" :";
		String rssmsg = ":"+rssBot+"!";
		String release = null;
		String torrent = null;
		String comment = null;
		long lastReconnect = System.currentTimeMillis();
		while(true) {
			try {
				line = in.readLine();
				debug("READING: "+line);
				if(line==null) {
					try {in.close();}catch(Exception e1) {;}
					try {out.close();}catch(Exception e2) {;}
					try {ircSock.close();}catch(Exception e3) {;}
					if(!connect()) {
						System.err.println("Could not reconnect to "+ircServer+":"+ircPort);
						System.exit(-10);
					}
					line = in.readLine();
				}
			}
			catch(Exception e) {
				try {in.close();}catch(Exception e1) {;}
				try {out.close();}catch(Exception e2) {;}
				try {ircSock.close();}catch(Exception e3) {;}
				if(!connect()) {
					System.err.println("Could not reconnect to "+ircServer+":"+ircPort);
					System.exit(-10);
				}
				try {
					line = in.readLine();
				}
				catch(Exception e2) {;}
			}
			try {
				if(line.startsWith(rssmsg)) {
					if(line.indexOf(privmsg)!=-1) {
						int index = line.indexOf(privmsg)+privmsg.length();
						line = line.substring(index);
						line = removeFormatting(line);
						if(line.startsWith("Release: [Anime]")) {
							line = line.substring(16).trim();
							release = line;
							recentReleases.addElement(release);
							if(recentReleases.size()>25) {
								recentReleases.removeElementAt(0);
							}
						}
						else if(!onlyAnime && line.startsWith("Release:")) {
							line = line.substring(8).trim();
							release = line;
							recentReleases.addElement(release);
							if(recentReleases.size()>25) {
								recentReleases.removeElementAt(0);
							}
						}
						else if(line.startsWith("Torrent:")) {
							line = line.substring(8).trim();
							torrent = line;
						}
						else if(line.startsWith("Size:")) {
							index = line.indexOf("Comment:");
							if(index!=-1) {
								line = line.substring(index+8);
								comment = line;
							}
							else {
								comment = "";
							}
							// We assume that the "Size" field is the last one
							if(release!=null && torrent!=null && comment!=null) {
								processRSS(release,comment,torrent);
								release = null;
								torrent = null;
								comment = null;
							}
						}
						else {
							release = null;
							torrent = null;
							comment = null;
						}
					}
					else {
						if(line.indexOf("QUIT :*.net *.split")!=-1) {
							netsplit = true;
						}
						else if(line.indexOf(" JOIN :"+myChan)!=-1) {
							netsplit = false;
						}
						release = null;
						torrent = null;
						comment = null;
					}
				}
				else if(line.indexOf("353 "+myNick)!=-1) {
					if(netsplit && line.indexOf(rssBot)!=-1) {
						debug("Back from Split");
						netsplit = false;
					}
				}
				else if(line.indexOf("352 "+myNick+" "+myChan)!=-1) {
					if(line.indexOf(rssBot+"!")!=-1) {
						netsplit = false;
					}
				}
				else {
					if(line.startsWith("PING")) {
						pong(line);
					}
				}
			}
			catch(Exception e) {
				System.err.println("Non-fatal exception, could not parse RSS Bot's message");
				e.printStackTrace();
			}

			// we need to wait a little bit before attempting to reconnect in case we split from the RSS Bot
			if(netsplit && (System.currentTimeMillis()-lastReconnect)>netsplitTimer) {
				synchronized(rizonPool) {
					ircServer = (String)rizonPool.elementAt(0);
					rizonPool.addElement(ircServer);
					rizonPool.removeElementAt(0);
				}
				// We got netsplitted away from the RSS bot, need to reconnect
				try {in.close();}catch(Exception e) {;}
				try {out.close();}catch(Exception e) {;}
				try {ircSock.close();}catch(Exception e) {;}
				lastReconnect = System.currentTimeMillis();
			}
		}
	}

	/**
	 * Parses out the name, torrent URL, and comment, then determine if this matches
	 * and of the match strings.
	 */
	public void processRSS(String release, String comment, String torrent)
	{
		debug("Checking: "+release+", "+comment);
		boolean releaseMatch = true;
		boolean commentMatch = true;
		boolean download = false;
		String lrelease = release.toLowerCase();
		String lcomment = comment.toLowerCase();
		synchronized(matchPatterns) {
			for(int x=0;x<matchPatterns.size();x++) {
				String[] patterns = (String[])matchPatterns.elementAt(x);
				for(int z=0;z<patterns.length;z++) {
					if(releaseMatch && ((patterns[z].startsWith("^") && lrelease.indexOf(patterns[z])!=-1) || 
						(!patterns[z].startsWith("^") && lrelease.indexOf(patterns[z])==-1))) {
						releaseMatch = false;
					}
					if(commentMatch && ((patterns[z].startsWith("^") && lcomment.indexOf(patterns[z])!=-1) || 
						(!patterns[z].startsWith("^") && lcomment.indexOf(patterns[z])==-1))) {
						commentMatch = false;
					}
					if(!commentMatch && !releaseMatch) {
						break;
					}
				}
				if(releaseMatch || commentMatch) {
					download = true;
					break;
				}
				releaseMatch = true;
				commentMatch = true;
			}
		}
		if(download) {
			if(download(release,comment,torrent)) {
				if(localPort>0) {
					synchronized(recentDownloads) {
						recentDownloads.addElement("<TD>"+dateFormat.format(new Date())+"</TD><TD><A HREF="+torrent+">"+encode(release)+"</A></TD><TD>"+encode(comment)+"</TD>");
						if(recentDownloads.size()>25) {
							recentDownloads.removeElementAt(0);
						}
					}
				}
				System.out.println("Downloaded: "+release+" - "+comment+" - "+torrent);
			}
			else {
				System.err.println("Could not download: "+torrent);
			}
		}
	}

	/**
	 * This gets called when a name, torrent URL, and comment matches and needs to be
	 * downloaded. This method calls the native downloader to download the torrent and store
	 * it in the torrent repository for the torrent program to pick up.
	 */
	public boolean download(String release, String comment, String url)
	{
		boolean rval = false;
		String flags = downloaderFlags;
		try {
			String file = System.currentTimeMillis()+".torrent";
			while(flags.indexOf("{file}")!=-1) {
				int index = flags.indexOf("{file}");
				int index2 = index+7;
				if(index2>flags.length()) {
					index2 = flags.length();
				}
				flags = flags.substring(0,index)+torrentDirectory+file+flags.substring(index2);
			}
			Runtime runtime = Runtime.getRuntime();
			Process process = runtime.exec(downloaderProgram+" "+flags+" "+url);
			if(process.waitFor()>=0) {
				// Just in case the runtime is slow
				Thread.sleep(500);
				rval = true;
				notify(release,comment,url,file);
			}
		}
		catch(Exception e) {
			System.err.println("Exception while trying to execute the downloader: "+downloaderProgram+" "+flags+" \""+url+"\"");
			e.printStackTrace();
		}
		return(rval);
	}

	/**
	 * In case notification is turned on, This sends a dirty email, using a trusted MTA.
	 */
	public void notify(String release, String comment, String torrent, String localfile)
	{
		Socket emailSock = null;
		BufferedReader emailIn = null;
		OutputStream emailOut = null;

		try {
			if(emailAddress!=null && mailRelay!=null) {
				emailSock = new Socket(mailRelay,25);
				emailIn = new BufferedReader(new InputStreamReader(emailSock.getInputStream()));
				emailOut = emailSock.getOutputStream();

				String line = emailIn.readLine();
				if(line.startsWith("220 ")) {
					emailOut.write(("HELO toshokan.bot.tesuji.org\n").getBytes());
					emailOut.flush();
					line = emailIn.readLine();
					if(line.startsWith("250 ")) {
						emailOut.write(("MAIL From: <"+emailAddress+">\n").getBytes());
						emailOut.flush();
						line = emailIn.readLine();
						if(line.startsWith("250 ")) {
							emailOut.write(("RCPT To: <"+emailAddress+">\n").getBytes());
							emailOut.flush();
							line = emailIn.readLine();
							if(line.startsWith("250 ")) {
								emailOut.write(("DATA\n").getBytes());
								emailOut.flush();
								line = emailIn.readLine();
								if(line.startsWith("354 ")) {
									emailOut.write(("From: "+emailAddress+"\nTo: "+emailAddress+"\nSubject: Tokyo Toshokan RSS bot: New Download\n\nNew torrent has been downloaded to: "+torrentDirectory+localfile+"\n\nDetails:\n"+release+"\n"+comment+"\n"+torrent+"\n\n.\n").getBytes());
									emailOut.flush();
									if(line.startsWith("250 ")) {
										emailOut.write(("QUIT\n").getBytes());
										debug("Email successfully sent to: "+emailAddress);
									}
								}
							}
						}
					}
				}
			}
		}
		catch(Exception e) {
			System.err.println("Exception while trying to send notification email");
			e.printStackTrace();
		}
	}					

	/**
	 * Responds to IRC server PINGs
	 */
	protected void pong(String line)
	{
		int index = line.lastIndexOf(" :");
		if(index!=-1) {
			write("PONG "+myPong+" "+line.substring(index).trim());
		}
	}

	/**
	 * Prints help to stdout.
	 */
	public void printHelp()
	{
		System.out.println("Synopsis: java org.jonlin.TokyoToshokanIRCBot <flags>");
		System.out.println("Flags:");
		System.out.println("  -nick <nickname>       This is the nick that the bot will use on the IRC server.");
		System.out.println("                         You must be sure to use one that hasn't already been taken,");
		System.out.println("                         otherwise the bot will not be able to join the channel.");
		System.out.println("  -channel <irc channel> The irc channel to join. This is typically #tokyotosho.");
		System.out.println("  -server <hostname>     This is the irc server to connect to. This is typically irc.rizon.net");
		System.out.println("  -port <port>           This is the port to connect to. This is typically between 6666-6669.");
		System.out.println("  -user <username>       The irc username to log into the server,");
		System.out.println("                         typically <name> <your hostname> <your fullhostname> :<irc name>");
		System.out.println("                         example: joe laptop laptop.mynetwork :joe_bot");
		System.out.println("  -pong <hostname>       The reply to use when pinged by the server,");
		System.out.println("                         typically your hostname. Example: mylaptop.mynetwork.com");
		System.out.println("  -rss <bot name>        This is the name of the bot in the channel that reports all the new");
		System.out.println("                         entries. These entries will be scanned and if there is a match, the");
		System.out.println("                         torrent will be downloaded.");
		//		System.out.println("  -rssname <bot ircname> This is the IRC name of the RSS bot. This is an extra measure added to");
		//		System.out.println("                         anyone from impersonating the RSS bot and confusing the Toshokan");
		//		System.out.println("                         torrent downloader. This is set to '~Python@Rizon-A0264FC6.resnet.nmt.edu'");
		//		System.out.println("                         by default and shouldn't need to be changed unless the RSS bot logs in");
		//		System.out.println("                         from a different location.");
		System.out.println("  -all                   This will make it so all types of releases are checked, and not just");
		System.out.println("                         the English language anime releases. This includes [Manga], [Non-English],");
		System.out.println("                         [Hentai], [Music], [Music-Videos], [Raws], and [Other]");
		System.out.println("  -downloader <program>  This is the name of the program to use when a torrent needs to be");
		System.out.println("                         downloaded. This is typically wget, but can be any program that can");
		System.out.println("                         be invoked on the command line and given a URL as a parameter.");
		System.out.println("                         See: http://www.gnu.org/software/wget/");
		System.out.println("  -flags <program flags> This is the flags to pass to the downloader program. This can include");
		System.out.println("                         the pattern '{file}' which will be replaced with the torrent filename.");
		System.out.println("                         Example to use with wget: -O {file}");
		System.out.println("  -patterns <file>       This is the file to store the patterns used to see if there's a torrent");
		System.out.println("                         being posted is one that you want to download. Patterns should be separated");
		System.out.println("                         by lines. Each line can contain multiple patterns separated by commas.");
		System.out.println("                         If all the patterns in one line matches a torrent name, then that torrent");
		System.out.println("                         will be downloaded. Patterns that start with a ^ are ones that MUST NOT");
		System.out.println("                         match. These patterns are case insensitive.");
		System.out.println("                         Example of a patterns file:");
		System.out.println("                                 death,note,[black-order],mkv");
		System.out.println("                                 hentai");
		System.out.println("                                 nodame,cantabile,froth-bite,^promo,^op,^ed");
		System.out.println("  -torrents <directory>  The directory for where the torrents that have been downloaded are to be");
		System.out.println("                         placed. This is typically the directory that your torrent program scans");
		System.out.println("                         for new torrents. Make sure that there is a trailing directory separator");
		System.out.println("                         at the end (/ for *nix, \\ for windows).");
		System.out.println("  -listen <port>         This will open a web interface on your localhost in order to give you the");
		System.out.println("                         ability to add and remove patterns as well as show you the list of recently");
		System.out.println("                         downloaded torrents. Without this flag, the web interface will not be");
		System.out.println("                         started. You can access the web interface only from the same computer you");
		System.out.println("                         are running the TokyoToshokanIRCBot on by going to http://localhost:<port>/");
		System.out.println("  -listenall             This flag causes the TokyoToshokanIRCBot to bind on all interfaces, and not");
		System.out.println("                         just localhost. This allows you to connect to the web interface from any");
		System.out.println("                         computer, instead of only the computer you are running this on. The -listen");
		System.out.println("                         MUST be set otherwise this flag is meaningless. Beware: If your computer is");
		System.out.println("                         visible from the internet, ANYONE can connect to the web interface and change");
		System.out.println("                         your match patterns.");
		System.out.println("  -email <email address> This is the email address to notify of when new torrents are downloaded.");
		System.out.println("                         No verification is done on the address itself, so make sure it is correct.");
		System.out.println("  -relay <mail host>     This is the email relay to use to send email. Typically, this is the same");
		System.out.println("                         server that you use in the 'Outgoing SMTP' field in your mail client. You");
		System.out.println("                         need to have send priviledges on the email relay from the computer you are");
		System.out.println("                         running this on, this bot will not attempt to authenticate with the email");
		System.out.println("                         relay. You need to have this and the -email flags set in order for email");
		System.out.println("                         notification to work.");
		System.out.println("  -debug                 This flag will cause the Toshokan bot to output debugging messages.");
		System.out.println("  -h or -help            Prints this help message.");
	}

	/**
	 * Save the current list of cached patterns to a file.
	 */
	public void savePatterns()
	{
		FileWriter fout = null;
		try {
			fout = new FileWriter(new File(matchPatternsFile));
			synchronized(matchPatterns) {
				for(int x=0;x<matchPatterns.size();x++) {
					String[] patterns = (String[])matchPatterns.elementAt(x);
					for(int z=0;z<patterns.length;z++) {
						if(z==0) {
							fout.write(patterns[z]);
						}
						else {
							fout.write(",");
							fout.write(patterns[z]);
						}
					}
					fout.write("\n");
					fout.flush();
				}
			}
		}
		catch(Exception e) {
			System.err.println("Error writing to patterns file: "+matchPatternsFile);
			e.printStackTrace();
		}
		finally {
			try {
				fout.close();
			}
			catch(Exception e) {;}
		}
	}

	/**
	 * Load the patterns to the cache from a file.
	 */
	public void loadPatterns()
	{
		BufferedReader fin = null;
		try {
			fin = new BufferedReader(new FileReader(new File(matchPatternsFile)));
			Vector tmp = new Vector();
			String line = fin.readLine();
			while(line!=null) {
				if(line.trim().length()>0) {
					Vector patterns = new Vector();
					int index = line.indexOf(",");
					while(index!=-1) {
						patterns.addElement(line.substring(0,index).trim().toLowerCase());
						line = line.substring(index+1).trim();
						index = line.indexOf(",");
					}
					if(line.trim().length()>0) {
						patterns.addElement(line.trim().toLowerCase());
					}
					String[] patternsArray = new String[patterns.size()];
					for(int x=0;x<patternsArray.length;x++) {
						patternsArray[x] = (String)patterns.elementAt(x);
					}
					tmp.addElement(patternsArray);
				}
				line = fin.readLine();
			}
			matchPatterns = tmp;
		}
		catch(Exception e) {
			System.err.println("Error reading from pattern file: "+matchPatternsFile);
			e.printStackTrace();
		}
		finally {
			try {
				fin.close();
			}
			catch(Exception e) {;}
		}
	}

	/**
	 * Used to connect to the next IRC server in the rizon pool. If the irc network isn't
	 * rizon, it attempts to reconnect to the same server.
	 */
	protected boolean connect()
	{
		boolean rval = false;
		String line = null;
		while(true) {
			try {
				System.out.println("Connecting to: "+ircServer+":"+ircPort);
				ircSock = new Socket(ircServer,ircPort);
			}
			catch(Exception e) {
				ircSock = null;
				System.err.println("Error connecting to "+ircServer+":"+ircPort);
				e.printStackTrace();
				if(rizonPool!=null) {
					synchronized(rizonPool) {
						ircServer = (String)rizonPool.elementAt(0);
						rizonPool.addElement(ircServer);
						rizonPool.removeElementAt(0);
					}
				}
			}
			if(ircSock!=null) {
				try {
					//					ircSock.setSoTimeout(60000);
					in = new BufferedReader(new InputStreamReader(ircSock.getInputStream()));
					out = ircSock.getOutputStream();
					
					write("NICK "+myNick);
					write("USER "+myUser);
					line = in.readLine();
					debug("READING: "+line);
					// read until we get to the VERSION
					while(line.indexOf("VERSION")==-1) {
						line = in.readLine();
					}
					write("JOIN "+myChan);
					rval = true;
					break;
				}
				catch(Exception e) {
					System.err.println("Error while signing on");
					e.printStackTrace();
				}
			}
		}
		return(rval);
	}

	protected void write(String s)
	{
		try {
			debug("SENDING: "+s);
			out.write((s+"\r\n").getBytes());
			out.flush();
		}
		catch(Exception e) {e.printStackTrace();}
	}

	protected void debug(String s)
	{
		if(debug) {
			System.out.println(s);
		}
	}

	protected String getUser(String input)
	{
		String rval = "";
		int index = input.indexOf("!");
		if(index!=-1) {
			rval = input.substring(1,index);
		}
		return(rval);
	}

	public String removeFormatting(String in)
	{
		StringBuffer out = new StringBuffer();
		boolean colorFormat = false;
		for(int x=0;x<in.length();x++) {
			char c = in.charAt(x);
			int i = ((int)c);
			if(!(i<10 || (i>10 && i<13) || (i>13 && i<32))) {
				out.append(c);
			}
			else if(i==3) {
				if(!colorFormat) {
					x++;
					if(x==in.length()) {
						break;
					}
					i=((int)in.charAt(x));
					if((i>=48 && i<=57) || (i>=65 && i<=70)) {
						colorFormat = true;
						x++;
						if(x==in.length()) {
							break;
						}
						if(!(i>=48 && i<=57)) {
							x--;
						}						
					}
					else {
						colorFormat = false;
						x--;
					}
				}
				else {
					x++;
					if(x==in.length()) {
						break;
					}
					i=((int)in.charAt(x));
					if((i>=48 && i<=57) || (i>=65 && i<=70)) {
						x++;
						if(x==in.length()) {
							break;
						}
						if(!(i>=48 && i<=57)) {
							x--;
						}						
					}
					else {
						colorFormat = false;
						x--;
					}
				}
			}
		}
		return(out.toString());
	}

	public String encode(String message)
	{
		StringBuffer rval = new StringBuffer();
		for(int x=0;x<message.length();x++) {
			char c = message.charAt(x);
			if(c=='&') {
				rval.append("&amp;");
			}
			else if(c=='"') {
				rval.append("&quot;");
			}
			else if(c=='>') {
				rval.append("&gt;");
			}
			else if(c=='<') {
				rval.append("&lt;");
			}
			else if(c=='\r') {
				// do nothing, we don't want these
			}
			else if(((int)c) > 127) {
				rval.append("&#"+((int)c)+";");
			}
			else {
				rval.append(c);
			}
		}
		return(rval.toString());
	}
	
	/**
	 * This class/thread listens on the local port for HTTP connections, then forks off to HTTPHandler
	 */
	class HTTPListener extends Thread
	{
		public HTTPListener()
		{
			start();
		}

		public void run()
		{
			ServerSocket ss = null;
			try {
				if(notLocalhost) {
					ss = new ServerSocket(localPort);
				}
				else {
					ss = new ServerSocket(localPort,10,InetAddress.getByName(null));
				}
				while(true) {
					try {
						Socket sock = ss.accept();
						new HTTPHandler(sock);
					}
					catch(Exception e) {
						System.err.println("Error accepting a connection, trying to rebind to port "+localPort);
						e.printStackTrace();
						ss.close();
						if(notLocalhost) {
							ss = new ServerSocket(localPort);
						}
						else {
							ss = new ServerSocket(localPort,10,InetAddress.getByName(null));
						}
					}
				}
			}
			catch(Exception e) {
				System.err.println("Cannot bind to port "+localPort+", is this port already in use?");
				e.printStackTrace();
			}
		}
	}

	/**
	 * This class/thread handles individual HTTP requests.
	 */
	class HTTPHandler extends Thread
	{
		InputStream httpIn;
		OutputStream httpOut;
		Socket sock;

		public HTTPHandler(Socket sck)
		{
			sock = sck;
			try {
				httpIn = sock.getInputStream();
				httpOut = sock.getOutputStream();
				start();
			}
			catch(Exception e) {;}
		}

		public String readLine() throws IOException
		{
			StringBuffer rval = new StringBuffer();
			char c = (char)httpIn.read();
			while(c != '\r' && c!= '\n') {
				rval.append(c);
				c = (char)httpIn.read();
				if(((int)c)==-1) {
					throw new IOException();
				}
			}
			if(c=='\r') {
				httpIn.read();
			}
			return(rval.toString());
		}

		public  String decode(String in)
		{
			StringBuffer rval = new StringBuffer();
			
			for(int x=0;x<in.length();x++) {
				char c = in.charAt(x);
				if(c=='+') {
					rval.append(" ");
				}
				else if(c=='%') {
					rval.append((char)hexToInt(in.substring(x+1,x+3)));
					x = x+2;
				}
				else {
					rval.append(c);
				}
			}				
			
			return(rval.toString());
		}

		protected int hexToInt(String hex)
		{
			if (hex.length() > 2 || hex.length() == 0) {
				return (-1);
			}
			return Integer.parseInt(hex, 16);
		}

		public Hashtable parseParams(String params)
		{
			Hashtable rval = new Hashtable();
			if(params.trim().length()>0) {
				if(params.indexOf(" ")!=-1) {
					params = params.substring(0,params.indexOf(" ")).trim();
				}
				int index = params.indexOf("&");
				while(index!=-1) {
					String pair = params.substring(0,index);
					params = params.substring(index+1);
					index = pair.indexOf("=");
					if(index!=-1) {
						rval.put(decode(pair.substring(0,index)),decode(pair.substring(index+1)));
					}
					else {
						rval.put(decode(pair),"");
					}
					index = params.indexOf("&");
				}
				if(params.length()>0) {
					index = params.indexOf("=");
					if(index!=-1) {
						rval.put(decode(params.substring(0,index)),com.cometway.httpd.HTMLStringTools.decode(params.substring(index+1)));
					}
					else {
						rval.put(decode(params),"");
					}
				}
			}
			return(rval);
		}

		public void httpWrite(String s) throws IOException
		{
			httpOut.write(s.getBytes());
		}

		public void run()
		{
			try {
				String params = "";
				String line = readLine();
				debug(sock.getInetAddress().getHostAddress()+" HTTP: "+line);
				if(line.startsWith("GET")) {
					line = line.substring(3).trim();
					String tmpLine = readLine();
					while(tmpLine.trim().length()>0) {
						tmpLine = readLine();
					}
				}
				else if(line.startsWith("POST")) {
					line = line.substring(4).trim();
					String tmpLine = readLine();
					int length = 0;
					while(tmpLine!=null && tmpLine.trim().length()>0) {
						if(tmpLine.toLowerCase().startsWith("content-length:")) {
							tmpLine = tmpLine.substring(15).trim();
							try {
								length = Integer.parseInt(tmpLine);
							}
							catch(Exception e) {e.printStackTrace();}
						}
						else if(tmpLine.trim().length()==0) {
							break;
						}
						tmpLine = readLine();
					}
					for(int x=0;x<length;x++) {
						params = params+((char)httpIn.read());
					}
				}
				else {
					line = null;
				}

				if(line!=null) {
					if(line.indexOf("?")!=-1) {
						params = params+line.substring(line.indexOf("?")+1);
						line = line.substring(0,line.indexOf("?"));
					}
					if(line.startsWith("/favicon.ico")) {
						try {
							httpOut.write(("HTTP/1.1 200 Ok\r\n").getBytes());
							httpOut.write(("Connection: close\r\n").getBytes());
							httpOut.write(("Content-Length: "+icon.length+"\r\n").getBytes());
							httpOut.write(("Content-Type: image/x-icon\r\n\r\n").getBytes());
							httpOut.write(icon);
							httpOut.flush();
						}
						catch(Exception e) {e.printStackTrace();}
					}
					else if(line.startsWith("/")) {
						if(params.trim().length()>0) {
							Hashtable parsedParams = parseParams(params);
							if(parsedParams.containsKey("opcode")) {
								String opcode = (String)parsedParams.get("opcode");
								if(opcode.equals("add")) {
									StringBuffer out = new StringBuffer();
									String pattern = (String)parsedParams.get("pattern");
									Vector patterns = new Vector();
									int index = line.indexOf(",");
									while(index!=-1) {
										patterns.addElement(pattern.substring(0,index).trim().toLowerCase());
										pattern = pattern.substring(index+1).trim();
										index = pattern.indexOf(",");
									}
									if(pattern.trim().length()>0) {
										patterns.addElement(pattern.trim().toLowerCase());
									}
									String[] patternsArray = new String[patterns.size()];
									for(int x=0;x<patternsArray.length;x++) {
										patternsArray[x] = (String)patterns.elementAt(x);
									}
									matchPatterns.addElement(patternsArray);
									savePatterns();
									
									out.append(getHeader(5));
									out.append("<H1>Added pattern: "+pattern+"</H1></BODY></HTML>");
									writeResponse(out.length());
									httpWrite(out.toString());
									httpOut.flush();
								}
								else if(opcode.equals("delete")) {
									StringBuffer out = new StringBuffer();
									int patternIndex = Integer.parseInt((String)parsedParams.get("index"));
									String[] pattern = null;
									synchronized(matchPatterns) {
										pattern = (String[])matchPatterns.elementAt(patternIndex);
										matchPatterns.removeElementAt(patternIndex);
										savePatterns();
									}
									out.append(getHeader(5));
									out.append("<H1>Removed pattern: ");
									for(int x=0;x<pattern.length;x++) {
										if(x==0) {
											out.append(pattern[x]);
										}
										else {
											out.append(",");
											out.append(pattern[x]);
										}
									}
									out.append("</H1></BODY></HTML>");
									writeResponse(out.length());
									httpWrite(out.toString());
									httpOut.flush();
								}
							}
						}
						else {
							StringBuffer out = new StringBuffer();

							out.append(getHeader(60));
							out.append("<H1>Recently Downloaded</H1>\n<TABLE WIDTH=90% BORDER=0>");
							StringBuffer tmp = new StringBuffer();
							synchronized(recentDownloads) {
								for(int x=0;x<recentDownloads.size();x++) {
									if(x%2==0) {
										tmp.append("<TR BGCOLOR=#DFDFDF>");
									}
									else {
										tmp.append("<TR BGCOLOR=#AFAFAF>");
									}
									tmp.append((String)recentDownloads.elementAt(x));
									tmp.append("</TR>\n");
								}
								if(recentDownloads.size()==0) {
									tmp.append("<TR><TD>No torrents have been downloaded yet</TD></TR>\n");
								}
							}
							out.append(tmp.toString());
							out.append("</TABLE>\n<HR>\n<H1>Recent Releases</H1>\n<TABLE WIDTH=90% BORDER=0>");
							tmp = new StringBuffer();
							synchronized(recentReleases) {
								for(int x=0;x<recentReleases.size();x++) {
									if(x%2==0) {
										tmp.append("<TR BGCOLOR=#DFDFDF><TD>");
									}
									else {
										tmp.append("<TR BGCOLOR=#AFAFAF><TD>");
									}
									tmp.append(encode((String)recentReleases.elementAt(x)));
									tmp.append("</TD></TR>\n");
								}
								if(recentReleases.size()==0) {
									tmp.append("<TR><TD>No Applicable Releases Posted Yet</TD></TR>\n");
								}
							}
							out.append(tmp.toString());
							out.append("</TABLE>\n<HR>\n<H1>Patterns used for matching</H1><TABLE BORDER=0>");
							tmp = new StringBuffer();
							synchronized(matchPatterns) {
								for(int x=0;x<matchPatterns.size();x++) {
									String[] patterns = (String[])matchPatterns.elementAt(x);
									if(x%2==0) {
										tmp.append("<TR BGCOLOR=#DFDFDF>");
									}
									else {
										tmp.append("<TR BGCOLOR=#AFAFAF>");
									}
									tmp.append("<TD>");
									for(int z=0;z<patterns.length;z++) {
										if(z==0) {
											tmp.append(encode(patterns[z]));
										}
										else {
											tmp.append(",");
											tmp.append(encode(patterns[z]));
										}
									}
									tmp.append("</TD><TD><A HREF=/?opcode=delete&index="+x);
									tmp.append(">DELETE PATTERN</A></TD></TR>\n");
								}
								if(matchPatterns.size()==0) {
									tmp.append("<TR><TD>No patterns have been entered yet. You can enter some using the form below.</TD></TR>\n");
								}
							}
							out.append(tmp.toString());
							out.append("</TABLE>\n<P>\n<FORM METHOD=POST ACTION=/><INPUT TYPE=HIDDEN NAME=opcode VALUE=add>Add new pattern: <INPUT TYPE=TEXT SIZE=80 NAME=pattern><INPUT TYPE=SUBMIT VALUE='Add Pattern'></FORM>\n<P>\nMultiple patterns can be used to further narrow down the torrents you want, they must be separated by a comma (,), which means you can't use commas in the patterns themselves. In order for a torrent to be downloaded, ALL of these comma separated patterns that you're about to enter must match either the torrent title (second column of table at the top) or the torrent comment (third column of the table at the top). Patterns that start with a '^' are patterns that MUST NOT match in order for the torrent to be downloaded. All patterns are matched in a case insensitive way. Example pattern: [Eclipse],Claymore,h264,.mkv,^promo,^v2</BODY></HTML>");
							writeResponse(out.length());
							httpWrite(out.toString());
							httpOut.flush();
						}
					}
				}
			}
			catch(Exception e) {
				System.err.println("Exception while handling web interface request");
				e.printStackTrace();
			}

			try {
				sock.close();
			}
			catch(Exception e) {;}
		}						

		public void writeResponse(int size) throws IOException
		{
			httpWrite("HTTP/1.1 200 Ok\r\nContent-Length: "+size);
			httpWrite("\r\nConnection: close\r\n\r\n");
			httpOut.flush();
		}
		
		public String getHeader(int seconds) throws IOException
		{
			return("<HTML><HEAD><META HTTP-EQUIV=Refresh CONTENT=\""+seconds+"; URL=/\"><TITLE>Tokyo Toshokan IRC RSS Reader and torrent downloader (by jonlin)</TITLE></HEAD><BODY>");
		}
	}



	public static void main(String[] args)
	{
		new TokyoToshokanIRCBot(args);
	}

		static final byte[] icon = {(byte)0,(byte)0,(byte)1,(byte)0,(byte)1,(byte)0,(byte)16,(byte)16,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)104,(byte)5,(byte)0,(byte)0,(byte)22,(byte)0,(byte)0,(byte)0,(byte)40,(byte)0,(byte)0,(byte)0,(byte)16,(byte)0,(byte)0,(byte)0,(byte)32,(byte)0,(byte)0,(byte)0,(byte)1,(byte)0,(byte)8,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)1,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)1,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)106,(byte)253,(byte)253,(byte)0,(byte)82,(byte)253,(byte)253,(byte)0,(byte)81,(byte)90,(byte)116,(byte)0,(byte)3,(byte)6,(byte)8,(byte)0,(byte)26,(byte)24,(byte)31,(byte)0,(byte)93,(byte)253,(byte)253,(byte)0,(byte)70,(byte)88,(byte)114,(byte)0,(byte)98,(byte)110,(byte)253,(byte)0,(byte)1,(byte)2,(byte)3,(byte)0,(byte)79,(byte)92,(byte)119,(byte)0,(byte)102,(byte)117,(byte)253,(byte)0,(byte)88,(byte)114,(byte)253,(byte)0,(byte)3,(byte)1,(byte)6,(byte)0,(byte)1,(byte)4,(byte)6,(byte)0,(byte)70,(byte)75,(byte)93,(byte)0,(byte)77,(byte)100,(byte)253,(byte)0,(byte)27,(byte)32,(byte)42,(byte)0,(byte)95,(byte)253,(byte)253,(byte)0,(byte)2,(byte)4,(byte)6,(byte)0,(byte)1,(byte)2,(byte)4,(byte)0,(byte)1,(byte)5,(byte)4,(byte)0,(byte)72,(byte)127,(byte)253,(byte)0,(byte)2,(byte)5,(byte)4,(byte)0,(byte)86,(byte)110,(byte)253,(byte)0,(byte)4,(byte)4,(byte)7,(byte)0,(byte)72,(byte)253,(byte)253,(byte)0,(byte)74,(byte)120,(byte)253,(byte)0,(byte)55,(byte)68,(byte)90,(byte)0,(byte)1,(byte)3,(byte)2,(byte)0,(byte)78,(byte)99,(byte)126,(byte)0,(byte)87,(byte)253,(byte)253,(byte)0,(byte)78,(byte)253,(byte)253,(byte)0,(byte)76,(byte)92,(byte)121,(byte)0,(byte)24,(byte)27,(byte)33,(byte)0,(byte)85,(byte)253,(byte)253,(byte)0,(byte)2,(byte)2,(byte)5,(byte)0,(byte)80,(byte)96,(byte)118,(byte)0,(byte)1,(byte)5,(byte)5,(byte)0,(byte)80,(byte)253,(byte)253,(byte)0,(byte)2,(byte)5,(byte)5,(byte)0,(byte)87,(byte)99,(byte)126,(byte)0,(byte)82,(byte)253,(byte)253,(byte)0,(byte)47,(byte)50,(byte)67,(byte)0,(byte)81,(byte)253,(byte)253,(byte)0,(byte)86,(byte)253,(byte)253,(byte)0,(byte)97,(byte)126,(byte)253,(byte)0,(byte)84,(byte)253,(byte)253,(byte)0,(byte)0,(byte)3,(byte)3,(byte)0,(byte)1,(byte)3,(byte)3,(byte)0,(byte)2,(byte)3,(byte)3,(byte)0,(byte)63,(byte)82,(byte)101,(byte)0,(byte)87,(byte)107,(byte)253,(byte)0,(byte)84,(byte)253,(byte)253,(byte)0,(byte)102,(byte)253,(byte)253,(byte)0,(byte)27,(byte)27,(byte)34,(byte)0,(byte)43,(byte)75,(byte)97,(byte)0,(byte)2,(byte)5,(byte)6,(byte)0,(byte)66,(byte)81,(byte)104,(byte)0,(byte)98,(byte)125,(byte)253,(byte)0,(byte)4,(byte)5,(byte)6,(byte)0,(byte)84,(byte)123,(byte)253,(byte)0,(byte)102,(byte)253,(byte)253,(byte)0,(byte)14,(byte)9,(byte)11,(byte)0,(byte)89,(byte)111,(byte)253,(byte)0,(byte)71,(byte)253,(byte)253,(byte)0,(byte)86,(byte)253,(byte)253,(byte)0,(byte)0,(byte)3,(byte)4,(byte)0,(byte)83,(byte)109,(byte)253,(byte)0,(byte)1,(byte)3,(byte)4,(byte)0,(byte)2,(byte)3,(byte)4,(byte)0,(byte)3,(byte)3,(byte)4,(byte)0,(byte)86,(byte)115,(byte)253,(byte)0,(byte)81,(byte)105,(byte)253,(byte)0,(byte)86,(byte)253,(byte)253,(byte)0,(byte)67,(byte)90,(byte)121,(byte)0,(byte)78,(byte)95,(byte)123,(byte)0,(byte)91,(byte)115,(byte)253,(byte)0,(byte)96,(byte)109,(byte)253,(byte)0,(byte)82,(byte)119,(byte)253,(byte)0,(byte)73,(byte)78,(byte)97,(byte)0,(byte)78,(byte)253,(byte)253,(byte)0,(byte)89,(byte)253,(byte)253,(byte)0,(byte)85,(byte)253,(byte)253,(byte)0,(byte)3,(byte)0,(byte)5,(byte)0,(byte)4,(byte)0,(byte)5,(byte)0,(byte)62,(byte)76,(byte)95,(byte)0,(byte)1,(byte)3,(byte)5,(byte)0,(byte)2,(byte)3,(byte)5,(byte)0,(byte)3,(byte)3,(byte)5,(byte)0,(byte)82,(byte)253,(byte)253,(byte)0,(byte)72,(byte)253,(byte)253,(byte)0,(byte)85,(byte)103,(byte)126,(byte)0,(byte)96,(byte)253,(byte)253,(byte)0,(byte)97,(byte)253,(byte)253,(byte)0,(byte)97,(byte)253,(byte)253,(byte)0,(byte)76,(byte)253,(byte)253,(byte)0,(byte)84,(byte)253,(byte)253,(byte)0,(byte)84,(byte)101,(byte)253,(byte)0,(byte)2,(byte)1,(byte)3,(byte)0,(byte)1,(byte)4,(byte)3,(byte)0,(byte)2,(byte)4,(byte)3,(byte)0,(byte)84,(byte)253,(byte)253,(byte)0,(byte)65,(byte)73,(byte)96,(byte)0,(byte)2,(byte)3,(byte)6,(byte)0,(byte)75,(byte)93,(byte)114,(byte)0,(byte)98,(byte)253,(byte)253,(byte)0,(byte)79,(byte)253,(byte)253,(byte)0,(byte)3,(byte)6,(byte)6,(byte)0,(byte)81,(byte)96,(byte)122,(byte)0,(byte)3,(byte)1,(byte)4,(byte)0,(byte)0,(byte)4,(byte)4,(byte)0,(byte)1,(byte)4,(byte)4,(byte)0,(byte)91,(byte)108,(byte)253,(byte)0,(byte)2,(byte)4,(byte)4,(byte)0,(byte)52,(byte)57,(byte)68,(byte)0,(byte)91,(byte)126,(byte)253,(byte)0,(byte)4,(byte)4,(byte)4,(byte)0,(byte)102,(byte)253,(byte)253,(byte)0,(byte)38,(byte)49,(byte)67,(byte)0,(byte)94,(byte)118,(byte)253,(byte)0,(byte)2,(byte)1,(byte)5,(byte)0,(byte)61,(byte)67,(byte)90,(byte)0,(byte)0,(byte)4,(byte)5,(byte)0,(byte)6,(byte)14,(byte)18,(byte)0,(byte)87,(byte)126,(byte)253,(byte)0,(byte)96,(byte)253,(byte)253,(byte)0,(byte)79,(byte)253,(byte)253,(byte)0,(byte)1,(byte)4,(byte)5,(byte)0,(byte)80,(byte)253,(byte)253,(byte)0,(byte)2,(byte)4,(byte)5,(byte)0,(byte)3,(byte)4,(byte)5,(byte)0,(byte)10,(byte)14,(byte)18,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)87,(byte)70,(byte)113,(byte)113,(byte)69,(byte)253,(byte)68,(byte)37,(byte)68,(byte)22,(byte)113,(byte)100,(byte)69,(byte)68,(byte)47,(byte)68,(byte)85,(byte)29,(byte)123,(byte)37,(byte)86,(byte)127,(byte)111,(byte)113,(byte)127,(byte)253,(byte)99,(byte)111,(byte)122,(byte)111,(byte)20,(byte)68,(byte)253,(byte)104,(byte)11,(byte)9,(byte)56,(byte)111,(byte)127,(byte)13,(byte)253,(byte)68,(byte)253,(byte)111,(byte)253,(byte)111,(byte)68,(byte)99,(byte)127,(byte)57,(byte)15,(byte)42,(byte)72,(byte)253,(byte)79,(byte)10,(byte)14,(byte)37,(byte)253,(byte)113,(byte)110,(byte)69,(byte)68,(byte)127,(byte)113,(byte)35,(byte)51,(byte)33,(byte)253,(byte)102,(byte)71,(byte)61,(byte)0,(byte)58,(byte)253,(byte)111,(byte)20,(byte)127,(byte)68,(byte)68,(byte)69,(byte)68,(byte)253,(byte)67,(byte)91,(byte)101,(byte)55,(byte)92,(byte)105,(byte)53,(byte)119,(byte)113,(byte)127,(byte)66,(byte)86,(byte)253,(byte)113,(byte)68,(byte)113,(byte)59,(byte)115,(byte)38,(byte)73,(byte)64,(byte)94,(byte)17,(byte)117,(byte)62,(byte)127,(byte)18,(byte)87,(byte)48,(byte)100,(byte)48,(byte)253,(byte)103,(byte)124,(byte)65,(byte)95,(byte)44,(byte)43,(byte)93,(byte)125,(byte)2,(byte)111,(byte)68,(byte)127,(byte)68,(byte)68,(byte)68,(byte)69,(byte)88,(byte)5,(byte)46,(byte)52,(byte)126,(byte)34,(byte)106,(byte)45,(byte)97,(byte)69,(byte)68,(byte)127,(byte)86,(byte)8,(byte)48,(byte)86,(byte)253,(byte)40,(byte)81,(byte)82,(byte)1,(byte)31,(byte)89,(byte)21,(byte)112,(byte)69,(byte)35,(byte)86,(byte)87,(byte)86,(byte)87,(byte)68,(byte)111,(byte)22,(byte)60,(byte)30,(byte)41,(byte)253,(byte)90,(byte)25,(byte)54,(byte)36,(byte)111,(byte)68,(byte)48,(byte)87,(byte)69,(byte)87,(byte)127,(byte)39,(byte)114,(byte)78,(byte)96,(byte)80,(byte)26,(byte)74,(byte)253,(byte)50,(byte)27,(byte)111,(byte)68,(byte)100,(byte)253,(byte)19,(byte)253,(byte)127,(byte)253,(byte)4,(byte)6,(byte)32,(byte)121,(byte)18,(byte)118,(byte)16,(byte)76,(byte)86,(byte)110,(byte)253,(byte)69,(byte)86,(byte)253,(byte)127,(byte)253,(byte)37,(byte)253,(byte)39,(byte)69,(byte)111,(byte)75,(byte)63,(byte)23,(byte)108,(byte)28,(byte)48,(byte)111,(byte)111,(byte)87,(byte)69,(byte)113,(byte)111,(byte)253,(byte)111,(byte)69,(byte)127,(byte)107,(byte)3,(byte)77,(byte)7,(byte)253,(byte)49,(byte)113,(byte)69,(byte)116,(byte)70,(byte)113,(byte)49,(byte)120,(byte)98,(byte)120,(byte)83,(byte)109,(byte)84,(byte)12,(byte)24,(byte)22,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0,(byte)0};


}
