using System;
using System.Collections;
using System.Data;
using System.Globalization;
using System.IO;
using System.Text;
using Orciid.Core.Util;
using Lucene.Net.Index;
using Lucene.Net.Documents;
using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Search;
using Lucene.Net.QueryParsers;

namespace Orciid.Core
{
	/// <summary>
	/// Full text indexing
	/// </summary>
	/// <remarks>
	/// This class manages the full text indexes for internal collections.  It uses
	/// dotLucene for the actual indexing.
	/// </remarks>
	public class FullTextIndex
	{
        /// <summary>
        /// Number
        /// </summary>
        /// <remarks>
        /// A sortable string representation of a number
        /// </remarks>
		public class Number
		{
            /// <summary>
            /// Converts a number to a string
            /// </summary>
            /// <remarks>
            /// This method converts a <see cref="long"/> number to a string that is
            /// indexable and correctly sortable
            /// </remarks>
            /// <param name="number">The number to store</param>
            /// <returns>The string representation of the number</returns>
			public static string ToIndex(long number)
			{
				if (number < 0)
					return 'n' + Convert.ToString(Int64.MaxValue + number + 1, 16).PadLeft(16, '0');
				else
					return 'p' + Convert.ToString(number, 16).PadLeft(16, '0');
			}

            /// <summary>
            /// Converts the string representation of a number back to a number
            /// </summary>
            /// <remarks>
            /// Will raise an <see cref="ArgumentException"/> if the given number has not
            /// been created using the <see cref="ToIndex"/> method.
            /// </remarks>
            /// <param name="number">The string representation of the number</param>
            /// <returns>The number</returns>
			public static long FromIndex(string number)
			{
				if (number.Length != 17)
					throw new ArgumentException("Not a valid indexed numeric value", "number");
				char prefix = number[0];
				if (prefix == 'n')
					return Convert.ToInt64(number.Substring(1), 16) - Int64.MaxValue - 1;
				else if (prefix == 'p')
					return Convert.ToInt64(number.Substring(1), 16);
				else
					throw new ArgumentException("Not a valid indexed numeric value", "number");
			}
		}

		/// <summary>
		/// Term information
		/// </summary>
		/// <remarks>
		/// This struct holds frequency information for terms within a full text index.
		/// It is used to sort terms by frequency.
		/// </remarks>
		public struct TermInfo: IComparable
		{
			private int freq;

			/// <summary>
			/// Frequency
			/// </summary>
			/// <remarks>
			/// The frequency of the term
			/// </remarks>
			/// <value>
			/// The frequency of the term
			/// </value>
			public int Frequency
			{
				get
				{
					return freq;
				}
			}

			private string term;

			/// <summary>
			/// Term
			/// </summary>
			/// <remarks>
			/// The term stored in the full text index
			/// </remarks>
			/// <value>
			/// The term stored in the full text index
			/// </value>
			public string Term
			{
				get
				{
					return term;
				}
			}

			/// <summary>
			/// Constructor
			/// </summary>
			/// <remarks>
			/// Creates a new TermInfo struct
			/// </remarks>
			/// <param name="t">Term</param>
			/// <param name="df">Frequency</param>
			public TermInfo(Term t, int df) 
			{
				term = t.Text();
				freq = df;
			}

            /// <summary>
            /// Constructor
            /// </summary>
            /// <remarks>
            /// Creates a new TermInfo struct
            /// </remarks>
            /// <param name="t">Term</param>
            /// <param name="df">Frequency</param>
			public TermInfo(string t, int df)
			{
				term = t;
				freq = df;
			}

			internal void IncrementFrequency()
			{
				freq++;
			}

			/// <summary>
			/// Comparison
			/// </summary>
			/// <remarks>
			/// Compares two TermInfo objects.
			/// </remarks>
			/// <param name="obj">A second TermInfo object</param>
			/// <returns>The result of the comparison of the TermInfo objects' frequency</returns>
			public int CompareTo(object obj)
			{
				TermInfo other = (TermInfo)obj;
				return Frequency.CompareTo(other.Frequency);
			}

            /// <summary>
            /// Comparer
            /// </summary>
            /// <remarks>
            /// Compares two TermInfo structures by their Term properties
            /// </remarks>
			public class AlphabeticComparer:
				IComparer
			{
                /// <summary>
                /// Compare TermInfo structures
                /// </summary>
                /// <remarks>
                /// Compares two TermInfo structures by their Term properties
                /// </remarks>
                /// <param name="a">First TermInfo structure</param>
                /// <param name="b">Second TermInfo structure</param>
                /// <returns>The result of <see cref="String.Compare(string, string)"/>
                /// called on the Term properties of the TermInfo structures</returns>
				public int Compare(object a, object b)
				{
					return String.Compare(((TermInfo)a).Term, ((TermInfo)b).Term);
				}
			}
		}

		private const string FTPATH = "__ftindex";

		private Collection[] collections;

		/// <summary>
		/// Junk words
		/// </summary>
		/// <remarks>
		/// A hashtable with junk words as keys.  Values are ignored and should be set to
		/// <c>null</c>.
		/// </remarks>
		/// <value>
		/// A hashtable with junk words as keys.
		/// </value>
		public static readonly Hashtable JunkWords;

		static FullTextIndex()
		{
			JunkWords = new Hashtable();
			JunkWords.Add("from", null);
			JunkWords.Add("yes", null);
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Creates a new FullTextIndex object for the specified collections.
		/// </remarks>
		/// <param name="c">One or more collections</param>
		public FullTextIndex(params Collection[] c)
		{
			collections = c;
		}

/*		private string GetIndexPath(Collection coll)
		{
			string root = coll.PhysicalResourcePathRoot;
			if (root == "")
				throw new CoreException("Physical root for collection does not exist");
			string path = Path.Combine(root, "__ftindex");
			if (!Directory.Exists(path))
				Directory.CreateDirectory(path);
			return path;
		}
*/
		internal string GetFullTextRoot(string collectionroot)
		{
			return Path.Combine(collectionroot, FTPATH);
		}
		
		internal string GetCurrentIndexPath(string collectionroot)
		{
			// make sure full-text index root directory exists
			string ftroot = GetFullTextRoot(collectionroot);
			if (!Directory.Exists(ftroot))
				return null;
			// find latest full-text index
			int latest = -1;
			string latestdir = null;
			foreach (string dir in Directory.GetDirectories(ftroot, "idx_????????"))
			{
				try
				{
					int idxnum = Int32.Parse(dir.Substring(dir.Length - 8));
					if (idxnum > latest)
					{
						latest = idxnum;
						latestdir = dir;
					}
				}
				catch (FormatException)
				{
					continue;
				}
			}
			return latestdir;
		}

		internal string GetNewIndexPath(string collectionroot)
		{
			string ftroot = GetFullTextRoot(collectionroot);
			string newpath;
			// get current index path
			string current = GetCurrentIndexPath(collectionroot);
			// not found?
			if (current == null)
			{
				// create new one
				newpath = Path.Combine(ftroot, "idx_00000001");
			}
			else
			{
				// find lowest non-existing number greater than current
				int c = Int32.Parse(current.Substring(current.Length - 8));
				do
				{
					c++;
					newpath = Path.Combine(ftroot, String.Format("idx_{0:00000000}", c));
				} while (Directory.Exists(newpath));
			}
			return newpath;
		}

		internal void RemoveOldIndexDirectories(string collectionroot)
		{
			string ftroot = GetFullTextRoot(collectionroot);
			// get current index path
			string current = GetCurrentIndexPath(collectionroot);
			// not found? nothing to do then
			if (current == null)
				return;
			// get number of current index
			int c = Int32.Parse(current.Substring(current.Length - 8));
			// delete all index directories with a lower number
			foreach (string dir in Directory.GetDirectories(ftroot, "idx_????????"))
			{
				try
				{
					int idxnum = Int32.Parse(dir.Substring(dir.Length - 8));
					if (idxnum < c)
						Directory.Delete(dir, true);
				}
				catch
				{
					// ignore all errors
					continue;
				}
			}
		}

		// TODO: add events to watch rebuild and update progress

		private class DocumentWriter
		{
			private DBConnection conn;
			private IndexWriter writer;

			public DocumentWriter(DBConnection conn, IndexWriter writer)
			{
				this.conn = conn;
				this.writer = writer;
			}

			private Document doc = null;
			private ImageIdentifier last = new ImageIdentifier(0, 0);
			private int lastid = 0;
			private StringBuilder alldata = new StringBuilder();

			public void AddRow(DataRow row)
			{
				lastid = conn.DataToInt(row["ID"], Int32.MaxValue);
				ImageIdentifier current = new ImageIdentifier(
					conn.DataToInt(row["ImageID"], 0),
					conn.DataToInt(row["CollectionID"], 0));
				string val = conn.DataToString(row["FieldValue"]);
				int startdate = conn.DataToInt(row["StartDate"], Int32.MinValue);
				int enddate = conn.DataToInt(row["EndDate"], Int32.MaxValue);
				string origval = conn.DataToString(row["OriginalValue"]);
				string field = conn.DataToString(row["Name"]).ToLower();
				bool keywordsearchable = conn.DataToBool(row["KeywordSearchable"], false);
				DateTime created = conn.DataToDateTime(row["Created"], DateTime.MinValue);
				DateTime modified = conn.DataToDateTime(row["Modified"], DateTime.MinValue);
				int owner = conn.DataToInt(row["UserID"], 0);

				if (val == null || val.Length == 0)
					return;

				if (current != last)
				{
					// add document to index
					Flush();
					// start new document
					doc = new Document();
					doc.Add(Lucene.Net.Documents.Field.Keyword("_id", current.ToString()));
					doc.Add(Lucene.Net.Documents.Field.Keyword("_coll", Number.ToIndex(current.CollectionID)));
					doc.Add(Lucene.Net.Documents.Field.Keyword("_owner", Number.ToIndex(owner)));
					doc.Add(Lucene.Net.Documents.Field.Keyword("_created", DateTools.DateToString(created, DateTools.Resolution.DAY)));
					doc.Add(Lucene.Net.Documents.Field.Keyword("_modified", DateTools.DateToString(modified, DateTools.Resolution.DAY)));
					last = current;
					alldata = new StringBuilder();
				}

				if (keywordsearchable)
				{
					alldata.Append(val).Append(' ');
					if (origval != null && origval.Length > 0)
						alldata.Append(origval).Append(' ');
				}

				if (startdate != Int32.MinValue)
					doc.Add(Lucene.Net.Documents.Field.Keyword(field + "__start", Number.ToIndex(startdate)));
				if (enddate != Int32.MaxValue)
					doc.Add(Lucene.Net.Documents.Field.Keyword(field + "__end", Number.ToIndex(enddate)));
				if (origval != null && origval.Length > 0)
					doc.Add(Lucene.Net.Documents.Field.Text(field + "__origval", origval));

				doc.Add(new Lucene.Net.Documents.Field(field, val, true, true, true, true));
			}

			public void Flush()
			{
				if (doc != null)
				{
					if (alldata.Length > 0)
						doc.Add(new Lucene.Net.Documents.Field("_all", alldata.ToString(), true, true, true, true));
					writer.AddDocument(doc);
				}
			}
		}

		const int BATCH_SIZE = 10000;
			
		private void IndexDocuments(DBConnection conn, IndexWriter writer, 
			Collection coll, DateTime modifiedsince)
		{
			Console.WriteLine("Indexing...");
			DocumentWriter docwriter = new DocumentWriter(conn, writer);
			int lastid = 0;
			ImageIdentifier last = new ImageIdentifier(0, 0);
			int rowindex = 0;
		
			while (true)
			{
				Query query = new Query(conn, @"
SELECT TOP {top} FieldData.ID,FieldData.ImageID,FieldData.CollectionID,
FieldData.FieldValue,FieldData.StartDate,FieldData.EndDate,FieldData.OriginalValue,
FieldDefinitions.Name,FieldDefinitions.KeywordSearchable,
Images.Created,Images.Modified,Images.UserID
FROM (FieldData JOIN FieldDefinitions ON FieldData.FieldID=FieldDefinitions.ID)
JOIN Images ON FieldData.ImageID=Images.ID AND FieldData.CollectionID=Images.CollectionID
WHERE FieldData.CollectionID={collid} 
AND (FieldData.ImageID>{lastimageid} OR 
	(FieldData.ImageID={lastimageid} AND FieldData.ID>{lastid}))
AND (Images.Modified>={modifiedsince} {@allimages})
ORDER BY FieldData.ImageID,FieldData.ID");
				query.AddParam("collid", coll.ID);
				query.AddParam("top", BATCH_SIZE);
				query.AddParam("lastimageid", last.ID);
				query.AddParam("lastid", lastid);
				query.AddParam("modifiedsince", modifiedsince);
				query.AddParam("@allimages", modifiedsince == DateTime.MinValue ? 
					"OR Images.Modified IS NULL" : "");

				DataTable table = conn.SelectQuery(query);
				if (table.Rows.Count == 0)
					break;
				
				foreach (DataRow row in table.Rows)
				{
					rowindex++;
					if (rowindex % 1000 == 0)
						Console.WriteLine(rowindex);
					docwriter.AddRow(row);
				}

				DataRow lastrow = table.Rows[table.Rows.Count - 1];

				last = new ImageIdentifier(
					conn.DataToInt(lastrow["ImageID"], 0),
					conn.DataToInt(lastrow["CollectionID"], 0));
				lastid = conn.DataToInt(lastrow["ID"], Int32.MaxValue);
			}
			docwriter.Flush();	
			Console.WriteLine("Indexed {0} rows", rowindex);
		}

        /// <summary>
        /// Rebuild indexes
        /// </summary>
        /// <remarks>
        /// Rebuild the indexes of all collections referenced by this object
        /// </remarks>
		public void RebuildIndex()
		{
			foreach (Collection coll in collections)
				RebuildIndex(coll);
		}

		private void RebuildIndex(Collection coll)
		{
			if (coll is IRemoteCollection)
				return;

			DateTime lastindexed = DateTime.Now;
			Properties props = Properties.GetProperties(coll);
			
			string physroot = coll.PhysicalResourcePathRoot;
			if (physroot == "")
			{
				TransactionLog.Instance.Add("Full-text index problem", 
					"Invalid resource path for collection " + coll.ID.ToString());
				return;
			}

			string ftroot = GetFullTextRoot(physroot);
			string temppath;
			int tempnum = 0;
			do 
			{
				tempnum++;
				temppath = Path.Combine(ftroot, String.Format("temp_{0}", tempnum));
			} while (Directory.Exists(temppath));
			Directory.CreateDirectory(temppath);
			
			Console.WriteLine("Indexing into {0}", temppath);
			IndexWriter writer = new IndexWriter(temppath, new StandardAnalyzer(), true);

			using (DBConnection conn = DBConnector.GetConnection())
				IndexDocuments(conn, writer, coll, DateTime.MinValue);

			writer.Optimize();
			writer.Close();

			// rename temporary directory to new index directory name
			Directory.Move(temppath, GetNewIndexPath(coll.PhysicalResourcePathRoot));
			
			props.Set("lastindexed", lastindexed.Ticks.ToString());

			// clean up old directories
			RemoveOldIndexDirectories(coll.PhysicalResourcePathRoot);
		}

        /// <summary>
        /// Update full-text index
        /// </summary>
        /// <remarks>
        /// Updates the full-text index for all collections referred to by this object
        /// </remarks>
		public void UpdateIndex()
		{
			foreach (Collection coll in collections)
				UpdateIndex(coll);
		}

		private void UpdateIndex(Collection coll)
		{
			if (coll is IRemoteCollection)
				return;

			DateTime updatestarted = DateTime.Now;
			Properties props = Properties.GetProperties(coll);
			DateTime lastindex;
			IndexReader reader = null;
			try
			{
				lastindex = new DateTime(long.Parse(props.Get("lastindexed", null)));
				reader = IndexReader.Open(GetCurrentIndexPath(coll.PhysicalResourcePathRoot));
			}
			catch
			{
				RebuildIndex(coll);
				return;
			}

			using (DBConnection conn = DBConnector.GetConnection())
			{
				// remove updated records from index
				Query query = new Query(conn,
					@"SELECT ID FROM Images 
					WHERE CollectionID={collid} AND Modified>={lastindex}");
				query.AddParam("collid", coll.ID);
				query.AddParam("lastindex", lastindex);
				int removed = 0;
				int[] updated = conn.TableToArray(conn.SelectQuery(query));
				if (updated != null)
					foreach (int imageid in updated)
						removed += reader.Delete(
							new Term("_id", new ImageIdentifier(imageid, coll.ID).ToString()));				
				reader.Close();

				Console.WriteLine("Removed {0} documents", removed);

				// adding updated records to index
				IndexWriter writer = new IndexWriter(
					GetCurrentIndexPath(coll.PhysicalResourcePathRoot), 
					new StandardAnalyzer(), false);
				IndexDocuments(conn, writer, coll, lastindex);
				writer.Optimize();
				writer.Close();
			}

			props.Set("lastindexed", updatestarted.Ticks.ToString());
		}

        /// <summary>
        /// Get Index Reader
        /// </summary>
        /// <remarks>
        /// Acquires an <see cref="IndexReader"/> for this full-text index
        /// </remarks>
        /// <returns>An <see cref="IndexReader"/> for this full-text index</returns>
		public IndexReader GetIndexReader()
		{
			try
			{
				if (collections.Length == 1)
					return IndexReader.Open(GetCurrentIndexPath(collections[0].PhysicalResourcePathRoot));
				ArrayList readers = new ArrayList(collections.Length);
				foreach (Collection coll in collections)
					readers.Add(IndexReader.Open(GetCurrentIndexPath(coll.PhysicalResourcePathRoot)));
				return new MultiReader((IndexReader[])readers.ToArray(typeof(IndexReader)));
			}
			catch (Exception ex)
			{
				TransactionLog.Instance.AddException(
					"Indexing", "Acquisition of IndexReader failed", ex);
				return null;
			}
		}

		/// <summary>
		/// Get high frequency terms
		/// </summary>
		/// <remarks>
		/// Determines the terms with the highest frequency across all fields in the index
		/// </remarks>
		/// <param name="numterms">Number of terms to be returned</param>
		/// <returns>An array of TermInfo structs</returns>
		public TermInfo[] GetHighFrequencyTerms(int numterms)
		{
			return GetHighFrequencyTerms(JunkWords, numterms, "_all");
		}

		/// <summary>
		/// Get high frequency terms
		/// </summary>
		/// <remarks>
		/// Determines the terms with the highest frequency across the specified
		/// fields in the index
		/// </remarks>
		/// <param name="numterms">Number of terms to be returned</param>
		/// <param name="field">The field for which to return the terms</param>
		/// <returns>An array of TermInfo structs</returns>
		public TermInfo[] GetHighFrequencyTerms(int numterms, string field)
		{
			return GetHighFrequencyTerms(JunkWords, numterms, field);
		}

		/// <summary>
		/// Get high frequency terms
		/// </summary>
		/// <remarks>
		/// Determines the terms with the highest frequency across the specified
		/// fields in the index while ignoring custom junk words
		/// </remarks>
		/// <param name="junkwords">A hashtable of junk words to be ignored</param>
		/// <param name="numterms">Number of terms to be returned</param>
		/// <param name="field">The field for which to return the terms</param>
		/// <returns>An array of TermInfo structs</returns>
		public TermInfo[] GetHighFrequencyTerms(Hashtable junkwords, int numterms, string field)
		{
			if (field == null || field.Length == 0) 
				return null;
			IndexReader reader = GetIndexReader();
			ArrayList tiq = new ArrayList();
			TermEnum terms = reader.Terms();        
			while (terms.Next()) 
			{
				Term term = terms.Term();
				string text = term.Text();
				if (text.Length < 3 || (junkwords != null && junkwords.ContainsKey(text)))
					continue;
				if (field == term.Field()) 
					tiq.Add(new TermInfo(term, terms.DocFreq()));
			}
			tiq.Sort();
			reader.Close();
			return (TermInfo[])tiq.GetRange(Math.Max(tiq.Count - numterms, 0), 
				Math.Min(numterms, tiq.Count)).ToArray(typeof(TermInfo));
		}

        /// <summary>
        /// Term limit
        /// </summary>
        /// <remarks>
        /// Used by the <see cref="GetHighFrequencyTerms(Hashtable, int, string, TermLimit[])"/> method to limit the results
        /// to records that match other given terms
        /// </remarks>
		public struct TermLimit
		{
            /// <summary>
            /// Field
            /// </summary>
            /// <remarks>
            /// The field of the limiting term
            /// </remarks>
			public readonly string Field;
            /// <summary>
            /// Term
            /// </summary>
            /// <remarks>
            /// The limiting term
            /// </remarks>
			public readonly string Term;

            /// <summary>
            /// Constructor
            /// </summary>
            /// <remarks>
            /// Creates a new TermLimit structure
            /// </remarks>
            /// <param name="field">The field of the limiting term</param>
            /// <param name="term">The limiting term</param>
			public TermLimit(string field, string term)
			{
				Field = field;
				Term = term;
			}
		}

        /// <summary>
        /// Get high frequency terms
        /// </summary>
        /// <remarks>
        /// Determines the terms with the highest frequency across the specified
        /// fields in the index while ignoring custom junk words
        /// </remarks>
        /// <param name="junkwords">A hashtable of junk words to be ignored</param>
        /// <param name="numterms">Number of terms to be returned</param>
        /// <param name="field">The field for which to return the terms</param>
        /// <param name="limits">Limits the results to the terms that appear in records
        /// that match these limiting terms</param>
        /// <returns>An array of TermInfo structs</returns>
		public TermInfo[] GetHighFrequencyTerms(Hashtable junkwords, int numterms, string field,
			params TermLimit[] limits)
		{
			if (field == null || field.Length == 0) 
				return null;
			IndexReader reader = GetIndexReader();
			ArrayList tiq = new ArrayList();

			int[] docids = null;
			bool first = true;

			foreach (TermLimit limit in limits)
			{
				ArrayList ids = new ArrayList();
				TermDocs docs = reader.TermDocs(new Term(limit.Field, limit.Term));
				while (docs.Next())
					ids.Add(docs.Doc());
				if (first)
				{
					docids = (int[])ids.ToArray(typeof(int));
					first = false;
				}
				else
					docids = IntegerListTools.Intersect(docids, (int[])ids.ToArray(typeof(int)));
			}

			if (docids == null)
				return null;

			Hashtable result = new Hashtable();

			foreach (int docid in docids)
			{
				TermFreqVector termfreqs = reader.GetTermFreqVector(docid, field);
				if (termfreqs == null)
					continue;
				foreach (string term in termfreqs.GetTerms())
				{
					if (result.ContainsKey(term))
						result[term] = ((int)result[term]) + 1;
					else
						result.Add(term, 1);
				}
			}
			
			foreach (TermLimit limit in limits)
				result.Remove(limit.Term.ToLower());

			foreach (string term in result.Keys)
				tiq.Add(new TermInfo(term, (int)result[term]));
			tiq.Sort();
			reader.Close();
			return (TermInfo[])tiq.GetRange(Math.Max(tiq.Count - numterms, 0), 
				Math.Min(numterms, tiq.Count)).ToArray(typeof(TermInfo));
		}

        /// <summary>
        /// Initialize full-text indexing
        /// </summary>
        /// <remarks>
        /// This method schedules the background tasks that build and update the full-text indexes
        /// </remarks>
		[Initialize]
		public static void Initialize()
		{			
			ScheduleFullTextIndexCheckAndUpdateTask();
			// this will schedule the full text index rebuild when the config file is loaded and subsequently changed
			Configuration.Instance.OnChange += new OnConfigurationChangeDelegate(Configuration_OnChange);
		}

		const string INDEX_TASK_TITLE = "Full-text index rebuild";

		private static void Configuration_OnChange(IConfiguration configuration, object arg)
		{
			// configuration file has changed, so reschedule background task
			// first, find currently scheduled task
			BackgroundTask task = BackgroundTask.FindTask(INDEX_TASK_TITLE);
			if (task != null)
			{
				try
				{
					// task found, so try to kill it
					task.Dequeue();
				}
				catch
				{
					// task could not be killed, so it is probably running right now
					// and will reschedule itself anyway
					return;
				}
			}
			// schedule task
			ScheduleFullTextIndexRebuildTask();
		}

		private static void ScheduleFullTextIndexCheckAndUpdateTask()
		{
			BackgroundTask task = new BackgroundTask(
				"Full-text index check and update", -5, "ftindex",
				DateTime.Now + TimeSpan.FromSeconds(30));
			task.OnRun += new BackgroundTaskDelegate(FullTextIndexCheckAndUpdateTask);
			task.Queue();
		}

		private static void ScheduleFullTextIndexRebuildTask()
		{
			string d = 
				Configuration.Instance.ContainsKey("search.lucene.fullindex.days") ?
				Configuration.Instance.GetString("search.lucene.fullindex.days") :
				"";
			string s = 
				Configuration.Instance.ContainsKey("search.lucene.fullindex.start") ?
				Configuration.Instance.GetString("search.lucene.fullindex.start") :
				"";

			Scheduler.Weekdays days = Scheduler.Weekdays.None;
			foreach (char c in d)
			{
				if (c >= '0' && c <= '6')
					days = days | (Scheduler.Weekdays)(1 << ((int)c - (int)'0'));
			}
			if (days == Scheduler.Weekdays.None)
				days = Scheduler.Weekdays.All;

			DateTime start;
			try
			{
				start = DateTime.ParseExact(
					s, 
					new string[] { "t", "T" }, 
					CultureInfo.CurrentCulture,
					DateTimeStyles.AllowWhiteSpaces);
			}
			catch
			{
				start = new DateTime(1, 1, 1, 2, 0, 0);
			}

			Scheduler.Rule rule = new Scheduler.Rule(days, start, TimeSpan.Zero, TimeSpan.Zero);			

			BackgroundTask task = new BackgroundTask(
				INDEX_TASK_TITLE, 
				-10, 
				"ftindex",
				new Scheduler(rule).GetNextTime());
			task.OnRun += new BackgroundTaskDelegate(FullTextIndexRebuildTask);
			task.Queue();
		}

		private static bool FullTextIndexCheckAndUpdateTask(BackgroundTask task, object info)
		{
			try
			{
				foreach (int id in Collection.GetCollectionIDs())
				{
					Collection coll = Collection.GetByID(false, id);
					if (coll != null && coll.Type == CollectionType.Internal)
						new FullTextIndex(coll).UpdateIndex();
				}
			}
			catch (Exception ex)
			{
				TransactionLog.Instance.AddException("Full-text index problem", "UpdateIndex failed", ex);
			}
			ScheduleFullTextIndexCheckAndUpdateTask();
			return true;
		}

		private static bool FullTextIndexRebuildTask(BackgroundTask task, object info)
		{
			try
			{
				RebuildAllIndexes();
			}
			catch (Exception ex)
			{
				TransactionLog.Instance.AddException("Full-text index problem", "RebuildAllIndexes failed", ex);
			}
			ScheduleFullTextIndexRebuildTask();
			return true;
		}
		
        /// <summary>
        /// Rebuild all indexes
        /// </summary>
        /// <remarks>
        /// Rebuilds the full-text indexes of all internal collections in the installation
        /// </remarks>
		public static void RebuildAllIndexes()
		{
			foreach (int id in Collection.GetCollectionIDs())
			{
				Collection coll = Collection.GetByID(false, id);
				if (coll != null && coll.Type == CollectionType.Internal)
					new FullTextIndex(coll).RebuildIndex();
			}
		}
	}
}
