
//
//  Trie.cs: An efficient exact string set matcher, using Aho-Corasick.  Used to
//  identify substrings that match Tomboy note titles as the user types.
//
//  To test, compile with:
//     mcs -g -o testtrie.exe -define:TEST Trie.cs
//

using System;
using System.Collections;

namespace Tomboy
{
	public class TrieHit
	{
		public int    Start;
		public int    End;
		public string Key;
		public object Value;

		public TrieHit (int start, int end, string key, object value)
		{
			Start = start;
			End = end;
			Key = key;
			Value = value;
		}
	}

	public class TrieTree
	{
		class TrieState
		{
			public TrieState Next;
			public TrieState Fail;
			public TrieMatch FirstMatch;
			public int       Final;
			public object    Id;
		}

		class TrieMatch
		{
			public TrieMatch Next;
			public TrieState State;
			public char      Value;
		}

		TrieState root;
		ArrayList fail_states;
		bool      case_sensitive;
		int       max_length;

		public TrieTree (bool case_sensitive)
		{
			this.case_sensitive = case_sensitive;

			root = new TrieState ();
			fail_states = new ArrayList (8);
		}

		TrieState InsertMatchAtState (int depth, TrieState q, char c)
		{
			// Create a new state with a failure at %root
			TrieState new_q = new TrieState ();
			new_q.Fail = root;

			// Insert/Replace into fail_states at %depth
			if (depth < fail_states.Count) {
				new_q.Next = (TrieState) fail_states [depth];
				fail_states [depth] = new_q;
			} else {
				fail_states.Insert (depth, new_q);
			}

			// New match points to the newly created state for value %c
			TrieMatch m = new TrieMatch ();
			m.Next = q.FirstMatch;
			m.State = new_q;
			m.Value = c;

			// Insert the new match into existin state's match list
			q.FirstMatch = m;

			return new_q;
		}

		// Iterate the matches at state %s looking for the first match
		// containing %c.
		TrieMatch FindMatchAtState (TrieState s, char c)
		{
			TrieMatch m = s.FirstMatch;

			while (m != null && m.Value != c)
				m = m.Next;

			return m;
		}

		/*
		 * final = empty set
		 * FOR p = 1 TO #pat
		 *   q = root
		 *   FOR j = 1 TO m[p]
		 *     IF g(q, pat[p][j]) == null
		 *       insert(q, pat[p][j])
		 *     ENDIF
		 *     q = g(q, pat[p][j])
		 *   ENDFOR
		 *   final = union(final, q)
		 * ENDFOR
		 */
		public void AddKeyword (string needle, object pattern_id)
		{
			TrieState q = root;
			int depth = 0;

			// Step 1: add the pattern to the trie...

			for (int idx = 0; idx < needle.Length; idx++) {
				char c = needle [idx];
				if (!case_sensitive)
					c = Char.ToLower (c);

				TrieMatch m = FindMatchAtState (q, c);
				if (m == null)
					q = InsertMatchAtState (depth, q, c);
				else
					q = m.State;

				depth++;
			}

			q.Final = depth;
			q.Id = pattern_id;

			// Step 2: compute failure graph...

			for (int idx = 0; idx < fail_states.Count; idx++) {
				q = (TrieState) fail_states [idx];

				while (q != null) {
					TrieMatch m = q.FirstMatch;

					while (m != null) {
						TrieState q1 = m.State;
						TrieState r = q.Fail;
						TrieMatch n = null;

						while (r != null) {
							n = FindMatchAtState (r, m.Value);
							if (n == null)
								r = r.Fail;
							else
								break;
						}

						if (r != null && n != null) {
							q1.Fail = n.State;

							if (q1.Fail.Final > q1.Final)
								q1.Final = q1.Fail.Final;
						} else {
							n = FindMatchAtState (root, m.Value);
							if (n == null)
								q1.Fail = root;
							else
								q1.Fail = n.State;
						}

						m = m.Next;
					}

					q = q.Next;
				}
			}

			// Update max_length
			max_length = Math.Max (max_length, needle.Length);
		}

		/*
		 * Aho-Corasick
		 *
		 * q = root
		 * FOR i = 1 TO n
		 *   WHILE q != fail AND g(q, text[i]) == fail
		 *     q = h(q)
		 *   ENDWHILE
		 *   IF q == fail
		 *     q = root
		 *   ELSE
		 *     q = g(q, text[i])
		 *   ENDIF
		 *   IF isElement(q, final)
		 *     RETURN TRUE
		 *   ENDIF
		 * ENDFOR
		 * RETURN FALSE
		 */
		public ArrayList FindMatches (string haystack)
		{
			ArrayList matches = new ArrayList (0);
			TrieState q = root;
			TrieMatch m = null;
			int idx = 0, start_idx = 0, last_idx = 0;

			while (idx < haystack.Length) {
				char c = haystack [idx++];
				if (!case_sensitive)
					c = Char.ToLower (c);

				while (q != null) {
					m = FindMatchAtState (q, c);
					if (m == null)
						q = q.Fail;
					else
						break;
				}

				if (q == root)
					start_idx = last_idx;

				if (q == null) {
					q = root;
					start_idx = idx;
				} else if (m != null) {
					q = m.State;

					// Got a match!
					if (q.Final != 0) {
						string key = haystack.Substring (start_idx,
						                                 idx - start_idx);
						TrieHit hit =
						        new TrieHit (start_idx, idx, key, q.Id);
						matches.Add (hit);
					}
				}

				last_idx = idx;
			}

			return matches;
		}

		public object Lookup (string key)
		{
			foreach (TrieHit hit in FindMatches (key)) {
				if (hit.Key.Length == key.Length)
					return hit.Value;
			}
			return null;
		}

		public int MaxLength
		{
			get {
				return max_length;
			}
		}
	}

	#if TEST
	public class Tester
	{
		public static void Main (string [] args)
		{
			string src = "bazar this is some foo, bar, and baz bazbarfoofoo bazbazarbaz end bazar";
			Logger.Log ("Searching in '{0}':", src);

			TrieTree trie = new TrieTree (false);
			trie.AddKeyword ("foo", "foo");
			trie.AddKeyword ("bar", "bar");
			trie.AddKeyword ("baz", "baz");
			trie.AddKeyword ("bazar", "bazar");

			Logger.Log ("Starting search...");
			foreach (TrieHit hit in trie.FindMatches (src)) {
				Logger.Log ("*** Match: '{0}' at {1}-{2}",
				            hit.Key,
				            hit.Start,
				            hit.End);
			}
			Logger.Log ("Search finished!");
		}
	}
	#endif
}
