/**
 * Copyright © 2008-2010, Res Interactive, LLC
 */
package com.tootsville.hangman;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.starhope.appius.game.AppiusClaudiusCaecus;
import org.starhope.appius.messaging.AbstractCensor;
import org.starhope.appius.sys.op.FilterResult;
import org.starhope.appius.sys.op.FilterStatus;

/**
 * Provides chat filtering.
 * 
 * @author cnicol, brpocock, theys
 */
public class Censor implements AbstractCensor {

	/**
	 * TODO: document this field (brpocock, Nov 5, 2009)
	 * 
	 * adjacentList (Censor)
	 */
	static volatile CopyOnWriteArraySet <String> adjacentList = new CopyOnWriteArraySet <String> ();
	/**
	 * TODO: document this field (brpocock, Nov 5, 2009)
	 * 
	 * blackList (Censor)
	 */
	static volatile CopyOnWriteArraySet <String> blackList = new CopyOnWriteArraySet <String> ();
	/**
	 * TODO: document this field (brpocock, Nov 5, 2009)
	 * 
	 * oneList (Censor)
	 */
	static volatile CopyOnWriteArraySet <String> oneList = new CopyOnWriteArraySet <String> ();
	/**
	 * TODO: document this field (brpocock, Nov 5, 2009)
	 * 
	 * weights (Censor)
	 */
	static volatile ConcurrentHashMap <String, Integer> weights = new ConcurrentHashMap <String, Integer> ();
	/**
	 * TODO: document this field (brpocock, Nov 5, 2009)
	 * 
	 * whiteList (Censor)
	 */
	static volatile CopyOnWriteArraySet <String> whiteList = new CopyOnWriteArraySet <String> ();

	/**
	 * TODO: document this field (brpocock, Nov 5, 2009)
	 * 
	 * worseList (Censor)
	 */
	static volatile CopyOnWriteArraySet <String> worseList = new CopyOnWriteArraySet <String> ();

	/**
	 * Add a word to the adjacent word list.
	 * 
	 * @param word WRITEME
	 */
	public static void addToAdjacentList (final String word) {
		Censor.adjacentList.add (word);
	}

	/**
	 * Add a word to the Black list.
	 * 
	 * @param word WRITEME
	 */
	public static void addToBlackList (final String word) {
		Censor.blackList.add (word);
	}

	/**
	 * Add a word to the White list.
	 * 
	 * @param word WRITEME
	 */
	public static void addToWhiteList (final String word) {
		Censor.whiteList.add (word);
	}

	/**
	 * Add a word to the Worse (Red) list.
	 * 
	 * @param word WRITEME
	 */
	public static void addToWorseList (final String word) {
		Censor.worseList.add (word);
	}

	/**
	 * Check if a token matches a full word in any of the filtering
	 * lists.
	 * 
	 * @param token to be checked.
	 * @return FilterResult indicating the kind of match found.
	 */
	private static FilterResult checkAdjacentLists (final String token) {
		// System.out.println("checking adjacent token: " + token);
		if (Censor.blackList.contains (token))
			return new FilterResult (FilterStatus.Black, token);
		if (Censor.worseList.contains (token))
			return new FilterResult (FilterStatus.Red, token);
		if (Censor.adjacentList.contains (token)) {
			System.out.println ("black");
			return new FilterResult (FilterStatus.Black, token);
		}
		return new FilterResult ();
	}

	/**
	 * TODO: document this method
	 * 
	 * @param tokens WRITEME
	 * @return WRITEME
	 */
	private static FilterResult checkAdjacentTokens (
			final List <String> tokens) {
		FilterResult result = new FilterResult ();

		for (int i = 0; i < tokens.size () - 1; ++i) {
			result = Censor.checkAdjacentLists (tokens.get (i)
					+ tokens.get (i + 1));
			if (FilterStatus.Ok != result.status) {
				break;
			}
		}

		return result;
	}

	/**
	 * 
	 * TODO: document this method (brpocock, Oct 13, 2009)
	 * 
	 * @return WRITEME
	 */
	public static int getBlackListLength () {
		return Censor.blackList.size ();
	}

	/**
	 * 
	 * TODO: document this method (brpocock, Oct 13, 2009)
	 * 
	 * @return WRITEME
	 */
	public static int getWorseListLength () {
		return Censor.worseList.size ();
	}

	/**
	 * Initialize the filtering word sets.
	 * 
	 */
	public static void init () {
		// This list is hard coded. Maybe not the greatest, but I did
		// not
		// want to allow it to be easily changed either.
		Censor.oneList.add ("a");
		Censor.oneList.add ("i");
		Censor.oneList.add ("o");
		Censor.oneList.add ("u");

		// Add some test data.
		// Censor.blackList.add ("age");

		// Censor.worseList.add ("fuck");
		// Censor.worseList.add ("ass");

		// Censor.whiteList.add ("mage");
		// Censor.whiteList.add ("page");
		// Censor.whiteList.add ("rage");
		// Censor.whiteList.add ("class");

		Censor.adjacentList.add ("eatme");

		Censor.weights.put ("eat", 2);
		Censor.weights.put ("me", 2);
		Censor.weights.put ("lick", 2);
		Censor.weights.put ("balls", 2);

	}

	/**
	 * Determine if the word is a white listed word.
	 * 
	 * @param word Word to check.
	 * @param copyOnWriteArraySet List to check.
	 * @return true if the word is in the list.
	 */
	private static boolean inWhitelist (final String word,
			final CopyOnWriteArraySet <String> copyOnWriteArraySet) {
		return copyOnWriteArraySet.contains (word);
	}

	/**
	 * TODO: document this method (brpocock, Oct 13, 2009)
	 * 
	 * @param dbh A live database connection from which to load the
	 *        censorship lists
	 */
	public static void loadLists (final Connection dbh) {

		Connection db = dbh;
		if (Censor.getBlackListLength () == 0) {
			Censor.init ();
		}

		// Load the filtering information from the database.

		boolean badLoad = false;

		do {
			try {
				if (null == db) {
					AppiusClaudiusCaecus
					.reportBug ("No DB connection available for Censorship init");
					return;
				}

				PreparedStatement cmdSelectWhiteList = null;
				ResultSet redList = null;
				ResultSet newWhiteList = null;
				ResultSet newBlackList = null;
				try {
					cmdSelectWhiteList = db
					.prepareStatement ("SELECT word FROM whitelist");
					newWhiteList = cmdSelectWhiteList.executeQuery ();
					while (newWhiteList.next ()) {
						Censor.addToWhiteList (newWhiteList
								.getString ("word"));
					}
				} catch (final SQLException e) {
					try {
						db.close ();
					} catch (final SQLException e1) {
						AppiusClaudiusCaecus
						.reportBug (
								"Caught a SQLException in loadLists and another closing Connection",
								e1);
					}
					db = null;
					throw AppiusClaudiusCaecus.fatalBug (e);
				} finally {
					if (null != newWhiteList) {
						try {
							newWhiteList.close ();
						} catch (final SQLException e) {
							AppiusClaudiusCaecus.reportBug (e);
						}
					}
					if (null != cmdSelectWhiteList) {
						try {
							cmdSelectWhiteList.close ();
						} catch (final SQLException e) {
							AppiusClaudiusCaecus.reportBug (e);
						}
					}
				}

				PreparedStatement cmdSelectBlackList = null;
				try {
					cmdSelectBlackList = db
					.prepareStatement ("SELECT word FROM new_blacklist WHERE severity='BAD'");
					newBlackList = cmdSelectBlackList.executeQuery ();

					while (newBlackList.next ()) {
						Censor.addToBlackList (newBlackList
								.getString ("word"));
					}
				} catch (final SQLException e) {
					try {
						db.close ();
					} catch (final SQLException e1) {
						AppiusClaudiusCaecus
						.reportBug (
								"Caught a SQLException in loadLists and another closing Connection",
								e1);
					}
					db = null;

					throw AppiusClaudiusCaecus.fatalBug (e);
				} finally {
					if (null != newBlackList) {
						try {
							newBlackList.close ();
						} catch (final SQLException e) {
							AppiusClaudiusCaecus.reportBug (e);
						}
					}
					if (null != cmdSelectBlackList) {
						try {
							cmdSelectBlackList.close ();
						} catch (final SQLException e) {
							AppiusClaudiusCaecus.reportBug (e);
						}
					}
				}

				PreparedStatement cmdSelectWorseList = null;
				try {
					cmdSelectWorseList = db
					.prepareStatement ("SELECT word FROM new_blacklist WHERE severity = 'WORSE'");
					redList = cmdSelectWorseList.executeQuery ();
					while (redList.next ()) {
						final String word = redList.getString ("word");
						Censor.addToWorseList (word);
					}
				} catch (final SQLException e) {
					AppiusClaudiusCaecus.fatalBug (e);
				} finally {
					if (null != redList) {
						try {
							redList.close ();
						} catch (final SQLException e) {
							AppiusClaudiusCaecus.reportBug (e);
						}
						// redList = null;
					}
					if (null != cmdSelectWorseList) {
						try {
							cmdSelectWorseList.close ();
						} catch (final SQLException e) {
							// Default catch action, report bug
							// (brpocock, Sep
							// 11, 2009)
							AppiusClaudiusCaecus.reportBug (e);
						}
					}
				}
			} catch (final RuntimeException e) {
				AppiusClaudiusCaecus.reportBug (
						"Failed to load lists for censors.", e);
				badLoad = true;
			}
		} while (badLoad);

		try {
			if (null != db) {
				db.close ();
			}
		} catch (final SQLException e1) {
			AppiusClaudiusCaecus
			.reportBug (
					"Caught a SQLException in loadLists and another closing Connection",
					e1);
		}
		db = null;

		System.err
		.println ("Censoring loaded\n\n\n\n Carl Nicol, 2008 — Res Interactive, LLC\n\n\n"
				+ Censor.whiteList.size ()
				+ " whitelist\n"
				+ Censor.blackList.size ()
				+ " warn words\n"
				+ Censor.worseList.size () + " kick words");
	}

	/**
	 * @param databaseConnection An open database connection over which
	 *        the censorship records can be loaded
	 */
	public static void prime (final Connection databaseConnection) {
		if (Censor.blackList.size () == 0) {
			Censor.loadLists (databaseConnection);
		} else {
			try {
				// TODO: encapsulate connections IN the Censor class
				databaseConnection.close ();
			} catch (final SQLException e) {
				AppiusClaudiusCaecus
				.reportBug (
						"Caught a SQLException in loadLists and another closing Connection",
						e);
			}
		}
	}

	/**
	 * @see org.starhope.appius.messaging.AbstractCensor#checkLists(java.lang.String)
	 */
	public FilterResult checkLists (final String token) {
		System.out.println ("checkLists token: '" + token + "'");

		final FilterResult result = new FilterResult ();

		result.status = FilterStatus.Black;
		for (final String expr : Censor.blackList) {
			final Pattern pat = Pattern.compile (expr);
			final Matcher m = pat.matcher (token);
			if (m.find ()) {
				if (!Censor.whiteList.contains (token)) {
					result.reason = token;
					return result;
				}
			}
		}

		result.status = FilterStatus.Red;
		for (final String expr : Censor.worseList) {
			final Pattern pat = Pattern.compile (expr);
			final Matcher m = pat.matcher (token);
			if (m.find ()) {
				if (!Censor.whiteList.contains (token)) {
					result.reason = token;
					return result;
				}
			}
		}

		result.status = FilterStatus.Ok;

		return result;
	}

	/**
	 * TODO: document this method
	 * 
	 * @param tokens WRITEME
	 * @return WRITEME
	 */
	private FilterResult checkTokens (final List <String> tokens) {
		final FilterResult result = new FilterResult ();
		result.status = org.starhope.appius.sys.op.FilterStatus.Ok;
		result.reason = "";
		CheckTokens: for (final String token : tokens) {
			if (token.length () == 1) {
				// Filter single letter words.
				if (!Censor.inWhitelist (token, Censor.oneList)) {
					result.reason = "invalid single letter '" + token
					+ "'";
					result.status = FilterStatus.Black;
					break CheckTokens;
				}
			} else {
				result.status = FilterStatus.Black;
				for (final String expr : Censor.blackList) {
					// System.out.println(expr);
					final Pattern pat = Pattern.compile (expr);
					final Matcher m = pat.matcher (token);
					if (m.find ()) {
						if (!Censor.whiteList.contains (token)) {
							result.reason = "'" + expr + "' matches '"
							+ token + "'";
							break CheckTokens;
						}
					}
				}

				result.status = FilterStatus.Red;
				for (final String expr : Censor.worseList) {
					final Pattern pat = Pattern.compile (expr);
					final Matcher m = pat.matcher (token);
					if (m.find ()) {
						if (!Censor.whiteList.contains (token)) {
							result.reason = "'" + expr + "' matches '"
							+ token + "'";
							break CheckTokens;
						}
					}
				}

			}
			// This token is ok.
			result.status = FilterStatus.Ok;
		}

		// System.out.println("checkTokens: " + result);
		return result;
	}

	/**
	 * Release the hash tables.
	 */
	public void destroy () {
		if (null != Censor.oneList) {
			Censor.oneList.clear ();
			Censor.oneList = null;
		}
		if (null != Censor.blackList) {
			Censor.blackList.clear ();
			Censor.blackList = null;
		}
		if (null != Censor.worseList) {
			Censor.worseList.clear ();
			Censor.worseList = null;
		}
		if (null != Censor.whiteList) {
			Censor.whiteList.clear ();
			Censor.whiteList = null;
		}

		if (null != Censor.adjacentList) {
			Censor.adjacentList.clear ();
			Censor.adjacentList = null;
		}

		if (null != Censor.weights) {
			Censor.weights.clear ();
			Censor.weights = null;
		}
	}

	/**
	 * @see org.starhope.appius.messaging.AbstractCensor#filterMessage(java.lang.String)
	 */
	public FilterResult filterMessage (final String text) {
		FilterResult result = new FilterResult ();
		result.status = org.starhope.appius.sys.op.FilterStatus.Ok;

		// Copy the unchanged message.
		String message = text;

		// Remove all numbers.
		final Pattern numberPattern = Pattern.compile ("\\d+");
		if (numberPattern.matcher (message).find ())
			return new FilterResult (FilterStatus.Black,
			"numbers found");

		// Convert to lowercase
		message = message.toLowerCase (Locale.ENGLISH);

		// Remove symbols
		final Pattern symbolsPattern = Pattern.compile ("[:,'!?.]+");
		message = symbolsPattern.matcher (message).replaceAll ("");

		// Tokenize and check each word.
		final String [] tokens = message.split ("\\s+");

		int score = 0;
		for (final String token : tokens) {
			if (Censor.weights.containsKey (token)) {
				final int weight = Censor.weights.get (token);
				score += weight;
				System.out.println ("filter:weight:" + token + ":"
						+ weight);
			}
		}
		if (score >= 4) {
			result.reason = "Multi-word filter";
			result.status = FilterStatus.Black;
		}

		final LinkedList <String> list = new LinkedList <String> ();
		for (final String token : tokens) {
			if (!Censor.whiteList.contains (token)) {
				list.add (token);
			}
		}

		result = checkTokens (list);
		if (FilterStatus.Ok == result.status) {
			result = Censor.checkAdjacentTokens (list);
		}
		return result;
	}

	/**
	 * @see org.starhope.appius.messaging.AbstractCensor#getWhiteListLength()
	 */
	public int getWhiteListLength () {
		return Censor.whiteList.size ();
	}
}
