using System;
using System.Data;
using System.Collections;
using System.Xml;
using System.Xml.Schema;
using System.IO;
using System.Text.RegularExpressions;
using Lucene.Net.Documents;

namespace Orciid.Core
{
	/// <summary>
	/// Search condition interface
	/// </summary>
	/// <remarks>
	/// All implemented search condition classes need to be derived from this class.
	/// It provides the actual search with necessary information to set up the SQL queries.
	/// </remarks>
	public abstract class SearchCondition 
	{
		/// <summary>
		/// Internal field name
		/// </summary>
		/// <remarks>
		/// The internal name of the <see cref="Field"/> this condition works on.
		/// </remarks>
		protected SearchField field;
		/// <summary>
		/// The field being searched
		/// </summary>
		/// <remarks>
		/// Most conditions work on a single field, which is represented by this property.
		/// </remarks>
		/// <value>
		/// The field being searched
		/// </value>
		public SearchField Field
		{
			get
			{
				return field;
			}
			set
			{
				field = value;
			}
		}

        /// <summary>
        /// The <see cref="SearchEngine"/> to use when running searches
        /// </summary>
        /// <remarks>
        /// Initially set to <see cref="Orciid.Core.SearchEngine.Default"/>
        /// </remarks>
		protected SearchEngine searchengine = SearchEngine.Default;

        /// <summary>
        /// The <see cref="SearchEngine"/> to use when running searches
        /// </summary>
        /// <remarks>
        /// Initially set to <see cref="Orciid.Core.SearchEngine.Default"/>
        /// </remarks>
        /// <value>
        /// The <see cref="SearchEngine"/> to use when running searches
        /// </value>
		public SearchEngine SearchEngine
		{
			get
			{
				return searchengine;
			}
			set
			{
				searchengine = value;
			}
		}

		/// <summary>
		/// SQL WHERE condition
		/// </summary>
		/// <remarks>
		/// The SQL WHERE condition to be used by the query to find matching records.
		/// </remarks>
		/// <returns>A condition to be used in an SQL WHERE statement</returns>
		internal abstract string WhereCondition();

		internal abstract string GetLuceneCondition(Collection coll);

        /// <summary>
        /// Insert field names into Lucene search condition
        /// </summary>
        /// <remarks>
        /// When performing cross-collection searches, the internal field names in the
        /// Lucene index may be different for equivalent fields in the different collections.
        /// This method inserts the correct field name for the given collection.
        /// </remarks>
        /// <param name="coll">The collection the search condition will be run on</param>
        /// <param name="condition">The condition to insert the field name into (the
        /// position of where the field name is to be inserted must be marked by <c>{0}</c>)</param>
        /// <returns>The condition with the field name inserted.  If multiple fields match the
        /// search condition, multiple copies of the condition chained by OR will be returned
        /// </returns>
		protected string InsertFieldNamesInLuceneCondition(Collection coll, string condition)
		{
			string[] names = coll.ResolveFieldName(this);
			if (names == null)
				return null;
			if (names.Length == 1)
				return String.Format(condition, names[0].ToLower());
			ArrayList list = new ArrayList();
			foreach (string name in names)
				list.Add(String.Format(condition, name.ToLower()));
			return "(" + String.Join(" OR ", (string[])list.ToArray(typeof(string))) + ")";
		}

		internal virtual bool SearchOnImageTable()
		{
			return false;
		}

		internal virtual SearchField FieldRestriction()
		{
			return field;
		}

		/// <summary>
		/// The condition as an XML element
		/// </summary>
		/// <remarks>
		/// This method returns this condition as an XML element
		/// </remarks>
		/// <param name="doc">The document element in which the condition will be placed</param>
		/// <returns>An XmlElement object representing this condition</returns>
		internal abstract XmlElement ToXml(XmlDocument doc);

		/// <summary>
		/// Creates a SearchCondition object from its XML representation
		/// </summary>
		/// <remarks>
		/// This method detects the type of condition stored in the XML node and
		/// creates the appropriate object.
		/// </remarks>
		/// <param name="e">The XmlNode containing the search condition</param>
		/// <returns>The appropriate search condition object, one of
		/// <list type="bullet">
		/// <item><see cref="ExactTextCondition"/></item>
		/// <item><see cref="TextCondition"/></item>
		/// <item><see cref="KeywordCondition"/></item>
		/// <item><see cref="DateCondition"/></item>
		/// </list></returns>
		/// <exception cref="CoreException">Thrown if the XmlNode does not
		/// have a known and understood tag name.</exception>
		public static SearchCondition FromXml(XmlNode e)
		{
			switch (e.Name)
			{
				case "text":
					XmlAttribute exact = e.Attributes["exact"];
					if (exact != null && exact.Value == "yes")
						return new ExactTextCondition(e);
					else
						return new TextCondition(e);
				case "keyword":
					return new KeywordCondition(e);
				case "date":
					return new DateCondition(e);
				case "recorddate":
					return new RecordDateCondition(e);
				case "owner":
					return new OwnerCondition(e);
				default:
					throw new CoreException("Invalid node name");
			}
		}

		/// <summary>
		/// Convert to string
		/// </summary>
		/// <remarks>
		/// This method returns a string representation of this search condition, useful
		/// for debugging purposes.
		/// </remarks>
		/// <returns>A string describing this search condition</returns>
		public override string ToString()
		{
			return base.ToString ();
		}
	}

	/// <summary>
	/// Owner search condition
	/// </summary>
	/// <remarks>
	/// This type of search condition finds images depending on their ownership status: 
	/// regular collection images (owned by nobody), images owned by the current user, and
	/// images owned by another user.
	/// </remarks>
	public class OwnerCondition:
		SearchCondition
	{
		private bool findpublic = false;
		private bool findown = false;
		private bool findothers = false;

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Constructor
		/// </remarks>
		public OwnerCondition()
		{
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Recreates an object from XML
		/// </remarks>
		/// <param name="node">The XML node representing a previously
		/// stored owner search condition</param>
		public OwnerCondition(XmlNode node):
			this()
		{
			foreach (XmlAttribute attr in node.Attributes)
			{
				switch (attr.Name)
				{
					case "public":
						findpublic = (attr.Value == "yes");
						break;
					case "own":
						findown = (attr.Value == "yes");
						break;
					case "others":
						findothers = (attr.Value == "yes");
						break;
					default:
						throw new CoreException("Invalid attribute");
				}
			}
		}

		/// <summary>
		/// Find public images
		/// </summary>
		/// <remarks>
		/// Find regular collection images that are not owned by anybody
		/// </remarks>
		/// <value>
		/// <c>true</c> if regular collection images will match, <c>false</c> otherwise
		/// </value>
		public bool FindPublic
		{
			get
			{
				return findpublic;
			}
			set
			{
				findpublic = value;
			}
		}

		/// <summary>
		/// Find images owned by current user
		/// </summary>
		/// <remarks>
		/// Find images that are owned by the current user
		/// </remarks>
		/// <value>
		/// <c>true</c> if images owned by the current user will match, <c>false</c> otherwise
		/// </value>
		public bool FindOwn
		{
			get
			{
				return findown;
			}
			set
			{
				findown = value;
			}
		}

		/// <summary>
		/// Find images owned by other users
		/// </summary>
		/// <remarks>
		/// Find images that are owned by other users, not including the current user
		/// </remarks>
		/// <value>
		/// <c>true</c> if images owned by other users will match, <c>false</c> otherwise
		/// </value>
		public bool FindOthers
		{
			get
			{
				return findothers;
			}
			set
			{
				findothers = value;
			}
		}

		internal override bool SearchOnImageTable()
		{
			return true;
		}

		internal override string WhereCondition()
		{
			User current = User.CurrentUser();
			if (findpublic)
			{
				if (findown)
				{
					if (findothers)
					{
						// all images
						return "1=1";
					}
					else
					{
						// public and own
						return String.Format("UserID IS NULL OR UserID=0 OR UserID={0}", current.ID);
					}
				}
				else
				{
					if (findothers)
					{
						// public and others
						return String.Format("UserID IS NULL OR UserID<>{0}", current.ID);
					}
					else
					{
						// public only
						return "UserID IS NULL OR UserID=0";
					}
				}
			}
			else if (findown)
			{
				if (findothers)
					// own and others
					return "UserID<>0";
				else
					// own only
					return String.Format("UserID={0}", current.ID);
			}
			else if (findothers)
			{
				// others only
				return String.Format("UserID<>0 AND UserID<>{0}", current.ID);
			}
			else
				// nothing at all
				return "0=1";
		}

		internal override string GetLuceneCondition(Collection coll)
		{
			User current = User.CurrentUser();
			if (findpublic)
			{
				if (findown)
				{
					if (findothers)
						// all images
						return null;
					else
						// public and own
						return String.Format("_owner:({0} OR {1})", 
							FullTextIndex.Number.ToIndex(0),
							FullTextIndex.Number.ToIndex(current.ID));
				}
				else
				{
					if (findothers)
						// public and others
						return String.Format("-_owner:{0}", 
							FullTextIndex.Number.ToIndex(current.ID));
					else
						// public only
						return String.Format("_owner:{0}",
							FullTextIndex.Number.ToIndex(0));
				}
			}
			else if (findown)
			{
				if (findothers)
					// own and others
					return String.Format("-_owner:{0}",
						FullTextIndex.Number.ToIndex(0));
				else
					// own only
					return String.Format("_owner:{0}",
						FullTextIndex.Number.ToIndex(current.ID));
			}
			else if (findothers)
			{
				// others only
				return String.Format("-_owner:{0} AND -_owner:{1}",
					FullTextIndex.Number.ToIndex(0),
					FullTextIndex.Number.ToIndex(current.ID));
			}
			else
				// nothing at all
				return String.Format("_owner:{0}",
					FullTextIndex.Number.ToIndex(-1));
		}


		internal override XmlElement ToXml(XmlDocument doc)
		{
			XmlElement e = doc.CreateElement("owner");
			XmlAttribute a;
			a = doc.CreateAttribute("public");
			a.Value = (findpublic ? "yes" : "no");
			e.Attributes.Append(a);
			a = doc.CreateAttribute("own");
			a.Value = (findown ? "yes" : "no");
			e.Attributes.Append(a);
			a = doc.CreateAttribute("others");
			a.Value = (findothers ? "yes" : "no");
			e.Attributes.Append(a);
			return e;
		}

		/// <summary>
		/// Convert to string
		/// </summary>
		/// <remarks>
		/// This method returns a string representation of this search condition, useful
		/// for debugging purposes.
		/// </remarks>
		/// <returns>A string describing this search condition</returns>
		public override string ToString()
		{
			return String.Format("OwnerCondition (public={0} own={1} others={2})",
				findpublic, findown, findothers);
		}
	}

	/// <summary>
	/// Image record creation or modification search condition
	/// </summary>
	/// <remarks>
	/// With this search condition it is possible to find image records that have been
	/// created or modified within an absolute date range or within a relative timespan.
	/// </remarks>
	public class RecordDateCondition:
		SearchCondition
	{
		private DateTime createdstart = DateTime.MinValue;
		private DateTime createdend = DateTime.MaxValue;
		private DateTime modifiedstart = DateTime.MinValue;
		private DateTime modifiedend = DateTime.MaxValue;
		private TimeSpan createdrelative = TimeSpan.MaxValue;
		private TimeSpan modifiedrelative = TimeSpan.MaxValue;
		
		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Default constructor
		/// </remarks>
		public RecordDateCondition()
		{
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Recreates a search condition object from XML created with the <see cref="ToXml"/> method.
		/// </remarks>
		/// <param name="node">The XML node representing the search condition object</param>
		public RecordDateCondition(XmlNode node):
			this()
		{
			foreach (XmlNode child in node.ChildNodes)
			{
				switch (child.Name)
				{
					case "createdafter":
						CreatedAfter = DateTime.Parse(child.InnerText);
						break;
					case "createdbefore":
						CreatedBefore = DateTime.Parse(child.InnerText);
						break;
					case "createdwithin":
						CreatedWithin = TimeSpan.FromDays(Double.Parse(child.InnerText));
						break;
					case "modifiedafter":
						ModifiedAfter = DateTime.Parse(child.InnerText);
						break;
					case "modifiedbefore":
						ModifiedBefore = DateTime.Parse(child.InnerText);
						break;
					case "modifiedwithin":
						ModifiedWithin = TimeSpan.FromDays(Double.Parse(child.InnerText));
						break;
					default:
						throw new CoreException("Invalid node name");
				}
			}
		}

		/// <summary>
		/// Match records created before a certain date
		/// </summary>
		/// <remarks>
		/// Setting this property will cause this search condition to match image records
		/// that have been created before a certain date
		/// </remarks>
		/// <value>
		/// The date before which matching records were created
		/// </value>
		public DateTime CreatedBefore
		{
			get
			{
				return createdend;
			}
			set
			{
				createdend = value;
				createdrelative = TimeSpan.MaxValue;
			}
		}

		/// <summary>
		/// Match records created after a certain date
		/// </summary>
		/// <remarks>
		/// Setting this property will cause this search condition to match image records
		/// that have been created after a certain date
		/// </remarks>
		/// <value>
		/// The date after which matching records were created
		/// </value>
		public DateTime CreatedAfter
		{
			get
			{
				return createdstart;
			}
			set
			{
				createdstart = value;
				createdrelative = TimeSpan.MaxValue;
			}
		}

		/// <summary>
		/// Match records modified before a certain date
		/// </summary>
		/// <remarks>
		/// Setting this property will cause this search condition to match image records
		/// that have been modified before a certain date
		/// </remarks>
		/// <value>
		/// The date before which matching records were modified
		/// </value>
		public DateTime ModifiedBefore
		{
			get
			{
				return modifiedend;
			}
			set
			{
				modifiedend = value;
				modifiedrelative = TimeSpan.MaxValue;
			}
		}

		/// <summary>
		/// Match records modified after a certain date
		/// </summary>
		/// <remarks>
		/// Setting this property will cause this search condition to match image records
		/// that have been modified after a certain date
		/// </remarks>
		/// <value>
		/// The date after which matching records were modified
		/// </value>
		public DateTime ModifiedAfter
		{
			get
			{
				return modifiedstart;
			}
			set
			{
				modifiedstart = value;
				modifiedrelative = TimeSpan.MaxValue;
			}
		}

		/// <summary>
		/// Match records created no longer than a certain timespan ago
		/// </summary>
		/// <remarks>
		/// Setting this property will cause this search condition to match image records
		/// that have been created no longer than the specified timespan ago.
		/// </remarks>
		/// <value>
		/// The timespan to use
		/// </value>
		public TimeSpan CreatedWithin
		{
			get
			{
				return createdrelative;
			}
			set
			{
				createdrelative = value;
				createdend = DateTime.MaxValue;
				createdstart = DateTime.Now - createdrelative;
			}
		}

		/// <summary>
		/// Match records modified no longer than a certain timespan ago
		/// </summary>
		/// <remarks>
		/// Setting this property will cause this search condition to match image records
		/// that have been modified no longer than the specified timespan ago.
		/// </remarks>
		/// <value>
		/// The timespan to use
		/// </value>
		public TimeSpan ModifiedWithin
		{
			get
			{
				return modifiedrelative;
			}
			set
			{
				modifiedrelative = value;
				modifiedend = DateTime.MaxValue;
				modifiedstart = DateTime.Now - modifiedrelative;
			}
		}

		internal override bool SearchOnImageTable()
		{
			return true;
		}

		internal override string WhereCondition()
		{
			ArrayList conditions = new ArrayList();
			using (DBConnection conn = DBConnector.GetConnection(false))
			{
				if (createdstart != DateTime.MinValue)
					conditions.Add(String.Format("(Created>='{0}')", 
						conn.DateToDBFormatString(createdstart)));
				if (createdend != DateTime.MaxValue)
					conditions.Add(String.Format("(Created<='{0}')",
						conn.DateToDBFormatString(createdend)));
				if (modifiedstart != DateTime.MinValue)
					conditions.Add(String.Format("(Modified>='{0}')",
						conn.DateToDBFormatString(modifiedstart)));
				if (modifiedend != DateTime.MaxValue)
					conditions.Add(String.Format("(Modified<='{0}')",
						conn.DateToDBFormatString(modifiedend)));
			}
			if (conditions.Count == 0)
				return "0=1";
			else
				return String.Join(" AND ", (string[])conditions.ToArray(typeof(string)));
		}

		internal override string GetLuceneCondition(Collection coll)
		{
			ArrayList conditions = new ArrayList();
			if (createdstart != DateTime.MinValue || createdend != DateTime.MaxValue)
				conditions.Add(String.Format("_created:{{{0} TO {1}}}",
					DateTools.DateToString(createdstart, DateTools.Resolution.DAY),
					DateTools.DateToString(createdend, DateTools.Resolution.DAY)));
			if (modifiedstart != DateTime.MinValue || modifiedend != DateTime.MaxValue)
				conditions.Add(String.Format("_modified:{{{0} TO {1}}}",
					DateTools.DateToString(modifiedstart, DateTools.Resolution.DAY),
					DateTools.DateToString(modifiedend, DateTools.Resolution.DAY)));
			if (conditions.Count == 0)
				return null;
			else
				return String.Join(" AND ", (string[])conditions.ToArray(typeof(string)));
		}

		internal override XmlElement ToXml(XmlDocument doc)
		{
			XmlElement e = doc.CreateElement("recorddate");
			if (createdrelative != TimeSpan.MaxValue)
			{
				XmlElement c = doc.CreateElement("createdwithin");
				c.InnerText = Math.Round(createdrelative.TotalDays).ToString();
				e.AppendChild(c);
			}
			else
			{
				if (createdstart != DateTime.MinValue)
				{
					XmlElement c = doc.CreateElement("createdafter");
					c.InnerText = createdstart.ToShortDateString();
					e.AppendChild(c);
				}
				if (createdend != DateTime.MaxValue)
				{
					XmlElement c = doc.CreateElement("createdbefore");
					c.InnerText = createdend.ToShortDateString();
					e.AppendChild(c);
				}
			}
			if (modifiedrelative != TimeSpan.MaxValue)
			{
				XmlElement c = doc.CreateElement("modifiedwithin");
				c.InnerText = Math.Round(modifiedrelative.TotalDays).ToString();
				e.AppendChild(c);
			}
			else
			{
				if (modifiedstart != DateTime.MinValue)
				{
					XmlElement c = doc.CreateElement("modifiedafter");
					c.InnerText = modifiedstart.ToShortDateString();
					e.AppendChild(c);
				}
				if (modifiedend != DateTime.MaxValue)
				{
					XmlElement c = doc.CreateElement("modifiedbefore");
					c.InnerText = modifiedend.ToShortDateString();
					e.AppendChild(c);
				}
			}
			return e;
		}

		/// <summary>
		/// Convert to string
		/// </summary>
		/// <remarks>
		/// This method returns a string representation of this search condition, useful
		/// for debugging purposes.
		/// </remarks>
		/// <returns>A string describing this search condition</returns>
		public override string ToString()
		{
			return String.Format("RecordDateCondition (createdstart={0} createdend={1} createdrelative={2} " +
				"modifiedstart={3} modifiedend={4} modifiedrelative={5})", 
				createdstart, createdend, createdrelative, modifiedstart, modifiedend, modifiedrelative);
		}
	}

/*
 * Performance of ID conditions is really bad, so they should not be used
 * 		
	/// <summary>
	/// Image identifier conditions
	/// </summary>
	/// <remarks>
	/// This class is used for special searches where only a specific subset of images
	/// is to be searched. The image identifiers for those images can be specified.
	/// </remarks>
	public class IDCondition:
		SearchCondition
	{
		/// <summary>
		/// Image identifiers
		/// </summary>
		/// <remarks>
		/// The image identifiers to find
		/// </remarks>
		protected ImageIdentifier[] imageidentifiers = null;

		/// <summary>
		/// Default constructor
		/// </summary>
		/// <remarks>
		/// This condition works on the FieldData table in the database.
		/// </remarks>
		public IDCondition()
		{
		}

		/// <summary>
		/// Image Identifiers
		/// </summary>
		/// <remarks>
		/// An array of image identifiers to match with this condition
		/// </remarks>
		/// <value>
		/// array of image identifiers
		/// </value>
		public ImageIdentifier[] ImageIdentifiers
		{
			get
			{
				return imageidentifiers;
			}
			set
			{
				imageidentifiers = value;
			}
		}

		/// <summary>
		/// Create SQL WHERE statement to find records
		/// </summary>
		/// <remarks>
		/// This method creates a WHERE statement containing all image identifiers
		/// listed in the <see cref="ImageIdentifiers"/> property.
		/// </remarks>
		/// <returns>a string containing the WHERE clause for the search query</returns>
		internal override string WhereCondition()
		{
			if (imageidentifiers == null || imageidentifiers.Length == 0)
				return "0=1";
			string[] wheres = new string[imageidentifiers.Length];
			for (int i = 0; i < imageidentifiers.Length; i++)
				wheres[i] = String.Format("(ImageID={0} AND FieldData.CollectionID={1})", 
					imageidentifiers[i].ID, imageidentifiers[i].CollectionID);
			return "(" + String.Join(" OR ", wheres) + ")";
		}

		internal override string GetLuceneCondition()
		{
			if (imageidentifiers == null || imageidentifiers.Length == 0)
				return null;
			string[] sid = new string[imageidentifiers.Length];
			for (int i = 0; i < imageidentifiers.Length; i++)
				sid[i] = imageidentifiers[i].ToString();
			return "_id:(" + String.Join(" OR ", sid) + ")";
		}

		internal override SearchField FieldRestriction()
		{
			return null;
		}

		/// <summary>
		/// The condition as an XML element 
		/// </summary>
		/// <remarks>
		/// This type of condition cannot be stored in XML, therefore this method
		/// always returns <c>null</c>.
		/// </remarks>
		/// <param name="doc">The document element in which the condition will be placed</param>
		/// <returns><c>null</c></returns>
		internal override XmlElement ToXml(XmlDocument doc)
		{
			return null;
		}

		/// <summary>
		/// Convert to string
		/// </summary>
		/// <remarks>
		/// This method returns a string representation of this search condition, useful
		/// for debugging purposes.
		/// </remarks>
		/// <returns>A string describing this search condition</returns>
		public override string ToString()
		{
			string s = "";
			if (imageidentifiers == null)
				s = "null";
			else
				foreach (ImageIdentifier i in imageidentifiers)
					s += (s.Length == 0 ? " " + i.ToString() : i.ToString());
			return String.Format("IDCondition (ids={0})", s);
		}
	}
*/
	/// <summary>
	/// Text conditions
	/// </summary>
	/// <remarks>
	/// This class is used for text field searches.
	/// </remarks>
	public class TextCondition:
		SearchCondition
	{
		/// <summary>
		/// Keyword
		/// </summary>
		/// <remarks>
		/// The keyword or phrase to find
		/// </remarks>
		protected string keyword;

		/// <summary>
		/// Default constructor
		/// </summary>
		/// <remarks>
		/// Text searches work on the FieldData table in the database.
		/// </remarks>
		public TextCondition()
		{
		}

		/// <summary>
		/// Creates a new TextCondition object from a XML representation
		/// </summary>
		/// <remarks>
		/// This constructor is the counter-part of the <see cref="TextCondition.ToXml(XmlDocument)"/> method.
		/// </remarks>
		/// <param name="node">The XmlNode containing the TextCondition information.</param>
		public TextCondition(XmlNode node):
			this()
		{
			field = new SearchField(node.FirstChild);
			keyword = node.Attributes["text"].Value;
		}

		/// <summary>
		/// Keyword
		/// </summary>
		/// <remarks>
		/// This property represents the keyword used in this condition
		/// </remarks>
		/// <value>
		/// The keyword to search for
		/// </value>
		/// <exception cref="CoreException">Thrown if the keyword is invalid, 
		/// null, or empty</exception>
		public string Keyword
		{
			get
			{
				return keyword;
			}
			set
			{				
				if (IsValidKeyword(value))
					keyword = value;
				else
					throw new CoreException("Invalid keyword");
			}
		}

		/// <summary>
		/// Check keyword validity
		/// </summary>
		/// <remarks>
		/// Not all keywords are valid for all full-text search engines.  This method checks
		/// if a given keyword is valid.
		/// </remarks>
		/// <param name="s">Keyword to check</param>
		/// <returns><c>true</c> if the keyword can be used in a search, 
		/// <c>false</c> otherwise</returns>
		protected virtual bool IsValidKeyword(string s)
		{
			if (s == null || s.Length == 0)
				return false;

			if (Configuration.Instance.ContainsKey("search.method") &&
				Configuration.Instance.GetString("search.method") == "lucene")
				return true;

			using (DBConnection conn = DBConnector.GetConnection(false))
				return conn.IsValidKeyword(s);
		}

		/// <summary>
		/// Create SQL WHERE statement to find keyword
		/// </summary>
		/// <remarks>
		/// Depending on the database, the keyword search works differently by using either LIKE
		/// statements or full-text search engines. If supported by the database, word forms of
		/// keywords will be found as well.
		/// </remarks>
		/// <returns>a string containing the WHERE clause for the search query</returns>
		internal override string WhereCondition()
		{
			using (DBConnection conn = DBConnector.GetConnection(false))
				return conn.TextComparison("FieldData.FieldValue", keyword);
		}

        /// <summary>
        /// Keyword with explicit <c>+</c> or <c>-</c> prefix
        /// </summary>
        /// <remarks>
        /// Used with the Lucene search engine to build syntactically correct queries
        /// </remarks>
		protected struct SignedKeyword
		{
            /// <summary>
            /// The sign of the keyword
            /// </summary>
            /// <remarks>
            /// <c>+</c> or <c>-</c>
            /// </remarks>
			public char sign;

            /// <summary>
            /// The keyword
            /// </summary>
            /// <remarks>
            /// The keyword that is required to be present or absent
            /// </remarks>
			public string keyword;
		}

        /// <summary>
        /// Convert a string to a <see cref="SignedKeyword"/> structure
        /// </summary>
        /// <remarks>
        /// This method ensures that Lucene search terms are correctly quoted and signed
        /// </remarks>
        /// <param name="keyword">The keyword with optional sign <c>+</c> or <c>-</c>.  
        /// If no sign is included, <c>+</c> is assumed.</param>
        /// <param name="quote">If <c>true</c>, the keyword will be enclosed in double quotes.</param>
        /// <returns>A <see cref="SignedKeyword"/> structure</returns>
		protected SignedKeyword GetSignedKeyword(string keyword, bool quote)
		{
			string k = keyword.ToLower();
			char sign = k[0];
			if (sign != '+' && sign != '-')
				sign = '+';
			else
				k = k.Substring(1);

			if (quote)
			{
				if (k[0] != '"' || k[k.Length - 1] != '"')
					k = "\"" + k + "\"";
			}
			
			SignedKeyword result;
			result.keyword = k;
			result.sign = sign;
			return result;
		}

		internal override string GetLuceneCondition(Collection coll)
		{
			SignedKeyword k = GetSignedKeyword(keyword, false);
			return InsertFieldNamesInLuceneCondition(coll, 
				String.Format("{1}{{0}}:({0})", k.keyword, k.sign));
		}

		/// <summary>
		/// The condition as an XML element
		/// </summary>
		/// <remarks>
		/// This method returns this condition as an XML element
		/// </remarks>
		/// <param name="doc">The document element in which the condition will be placed</param>
		/// <returns>An XmlElement object representing this condition</returns>
		internal override XmlElement ToXml(XmlDocument doc)
		{
			XmlElement e = doc.CreateElement("text");
			XmlAttribute a = doc.CreateAttribute("text");
			a.Value = keyword;
			e.Attributes.Append(a);
			e.AppendChild(field.ToXml(doc));
			return e;
		}

		/// <summary>
		/// Convert to string
		/// </summary>
		/// <remarks>
		/// This method returns a string representation of this search condition, useful
		/// for debugging purposes.
		/// </remarks>
		/// <returns>A string describing this search condition</returns>
		public override string ToString()
		{
			return String.Format("TextCondition (text='{0}')", keyword);
		}

		private class QuotedTermEvaluator
		{
			private ArrayList target;

			public string ReplaceAndRemember(Match m)
			{
				target.Add(m.Value);
				return "";
			}

			public MatchEvaluator GetEvaluator(ArrayList target)
			{
				this.target = target;
				return new MatchEvaluator(ReplaceAndRemember);
			}
		}

		internal static string[] SplitTermsLucene(string terms)
		{
			if (terms == null)
				return null;

			ArrayList splitterms = new ArrayList();

			Regex regex = new Regex("[+-]?\"[^\"]*\"");
			string unquoted = 
				regex.Replace(terms, new QuotedTermEvaluator().GetEvaluator(splitterms));

			splitterms.AddRange(unquoted.Split(null));

			for (int i = splitterms.Count - 1; i >= 0; i--)
			{
				string s = ((string)splitterms[i]).Trim();
				if (s == "" || s == "\"\"")
					splitterms.RemoveAt(i);
				else
					splitterms[i] = s;
			}
		
			return (string[])splitterms.ToArray(typeof(string));
		}

        /// <summary>
        /// Splits a string into individual terms
        /// </summary>
        /// <remarks>
        /// Depending on the search engine in use, terms are split differently.
        /// </remarks>
        /// <param name="terms">The string containing the terms</param>
        /// <returns>An array of strings representing individual terms</returns>
		public static string[] SplitTerms(string terms)
		{
			bool uselucene = Configuration.Instance.ContainsKey("search.method") &&
				Configuration.Instance.GetString("search.method") == "lucene";

			if (uselucene)
				return SplitTermsLucene(terms);

			char[] splitchars = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".ToCharArray();

			ArrayList splitterms = new ArrayList();
			foreach (string s in terms.Split(splitchars))
			{
				string t = s.Trim();
				if (t.Length > 0)
					splitterms.Add(t);
			}

			return (string[])splitterms.ToArray(typeof(string));
		}	
	}

	/// <summary>
	/// Exact text condition
	/// </summary>
	/// <remarks>
	/// This condition compares the complete field value, not just part of it
	/// </remarks>
	public class ExactTextCondition:
		TextCondition
	{
		private bool comparefullvalue = true;

		/// <summary>
		/// Default constructor
		/// </summary>
		/// <remarks>
		/// The default constructor for this class
		/// </remarks>
		public ExactTextCondition():
			base()
		{
			searchengine = SearchEngine.Database;
		}

		/// <summary>
		/// Creates a new ExactTextCondition object from a XML representation
		/// </summary>
		/// <remarks>
		/// This constructor is the counter-part of the <see cref="ExactTextCondition.ToXml(XmlDocument)"/> method.
		/// </remarks>
		/// <param name="node">The XmlNode containing the ExactTextCondition information.</param>
		public ExactTextCondition(XmlNode node):
			this()
		{
			field = new SearchField(node.FirstChild);
			keyword = node.Attributes["text"].Value;
			XmlAttribute cfvattr = node.Attributes["comparefullvalue"];
			if (cfvattr != null)
			{
				comparefullvalue = (cfvattr.Value == "yes");
				searchengine = (comparefullvalue ? SearchEngine.Database : SearchEngine.Default);
			}
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// ExactTextCondition can compare either a full field value or a substring of the field value
		/// </remarks>
		/// <param name="comparefullvalue"><c>true</c> if the keyword has to be the exact full
		/// field value to trigger a match, <c>false</c> if the keyword can be a substring of 
		/// the field value to trigger a match</param>
		public ExactTextCondition(bool comparefullvalue):
			this()
		{
			this.comparefullvalue = comparefullvalue;
			searchengine = (comparefullvalue ? SearchEngine.Database : SearchEngine.Default);
		}

		/// <summary>
		/// Check keyword validity
		/// </summary>
		/// <remarks>
		/// Not all keywords are valid for all full-text search engines.  This method checks
		/// if a given keyword is valid.
		/// </remarks>
		/// <param name="s">Keyword to check</param>
		/// <returns><c>true</c> if the keyword can be used in a search, 
		/// <c>false</c> otherwise</returns>
		protected override bool IsValidKeyword(string s)
		{
			return (s != null && s.Length > 0);
		}
		
		/// <summary>
		/// Create SQL WHERE statement to find exact text
		/// </summary>
		/// <remarks>
		/// Compares the complete field value using a LIKE condition
		/// </remarks>
		/// <returns>a string containing the WHERE clause for the search query</returns>
		internal override string WhereCondition()
		{
			using (DBConnection conn = DBConnector.GetConnection(false))
			{
				if (comparefullvalue)
					return String.Format("(FieldData.FieldValue LIKE {1}'{0}')", 
						conn.LikeEncode(keyword), conn.UnicodeStringPrefix());
				else
					return String.Format("(FieldData.FieldValue LIKE {1}'%{0}%')", 
						conn.LikeEncode(keyword), conn.UnicodeStringPrefix());
			}
		}

		internal override string GetLuceneCondition(Collection coll)
		{
			SignedKeyword k = GetSignedKeyword(keyword, false);
			return InsertFieldNamesInLuceneCondition(coll, 
				String.Format("{1}{{0}}:{0}", k.keyword, k.sign));
//			return String.Format("{0}:\"{1}\"",
//				field.Name.ToLower(),
//				keyword.ToLower());
		}

		/// <summary>
		/// The condition as an XML element
		/// </summary>
		/// <remarks>
		/// This method returns this condition as an XML element
		/// </remarks>
		/// <param name="doc">The document element in which the condition will be placed</param>
		/// <returns>An XmlElement object representing this condition</returns>
		internal override XmlElement ToXml(XmlDocument doc)
		{
			XmlElement e = doc.CreateElement("text");
			XmlAttribute a = doc.CreateAttribute("text");
			a.Value = keyword;
			e.Attributes.Append(a);
			a = doc.CreateAttribute("exact");
			a.Value = "yes";
			e.Attributes.Append(a);
			a = doc.CreateAttribute("comparefullvalue");
			a.Value = (comparefullvalue ? "yes" : "no");
			e.Attributes.Append(a);
			e.AppendChild(field.ToXml(doc));
			return e;
		}

		/// <summary>
		/// Convert to string
		/// </summary>
		/// <remarks>
		/// This method returns a string representation of this search condition, useful
		/// for debugging purposes.
		/// </remarks>
		/// <returns>A string describing this search condition</returns>
		public override string ToString()
		{
			return String.Format("ExactTextCondition (text='{0}' full={1})",
				keyword, comparefullvalue);
		}
	}

	/// <summary>
	/// Keyword conditions
	/// </summary>
	/// <remarks>
	/// This class is used for keyword searches.
	/// </remarks>
	public class KeywordCondition:
		TextCondition
	{
		/// <summary>
		/// Default constructor
		/// </summary>
		/// <remarks>
		/// Text searches work on the FieldData table in the database.
		/// </remarks>
		public KeywordCondition():
			base()
		{
		}

		/// <summary>
		/// Creates a new KeywordCondition object from a XML representation
		/// </summary>
		/// <remarks>
		/// This constructor is the counter-part of the <see cref="KeywordCondition.ToXml(XmlDocument)"/> method.
		/// </remarks>
		/// <param name="node">The XmlNode containing the KeywordCondition information.</param>
		public KeywordCondition(XmlNode node):
			this()
		{
			keyword = node.Attributes["text"].Value;
		}

		internal override SearchField FieldRestriction()
		{
			return null;
		}

		internal override string GetLuceneCondition(Collection coll)
		{
			SignedKeyword k = GetSignedKeyword(keyword, false);
			return String.Format("{1}_all:({0})", k.keyword, k.sign);
		}


		/// <summary>
		/// The condition as an XML element
		/// </summary>
		/// <remarks>
		/// This method returns this condition as an XML element
		/// </remarks>
		/// <param name="doc">The document element in which the condition will be placed</param>
		/// <returns>An XmlElement object representing this condition</returns>
		internal override XmlElement ToXml(XmlDocument doc)
		{
			XmlElement e = doc.CreateElement("keyword");
			XmlAttribute a = doc.CreateAttribute("text");
			a.Value = Keyword;
			e.Attributes.Append(a);
			return e;
		}
	
		/// <summary>
		/// Convert to string
		/// </summary>
		/// <remarks>
		/// This method returns a string representation of this search condition, useful
		/// for debugging purposes.
		/// </remarks>
		/// <returns>A string describing this search condition</returns>
		public override string ToString()
		{
			return String.Format("KeywordCondition (text='{0}')", keyword);
		}

/*		/// <summary>
		/// Split a list of keywords at invalid characters
		/// </summary>
		/// <remarks>
		/// Depending on the search engine used (SQL full-text, Lucene), keywords
		/// can contain different valid characters.  This method splits a string
		/// containing multiple keywords into individual keywords at invalid characters.
		/// </remarks>
		/// <param name="keywords">A string containing keywords</param>
		/// <returns>An array of individual keywords, some of which may be empty strings</returns>
		public static string[] SplitKeywords(string keywords)
		{
			if (keywords == null)
				return null;

			bool uselucene = Configuration.Instance.ContainsKey("search.method") &&
				Configuration.Instance.GetString("search.method") == "lucene";

			if (uselucene)
				return new string[] { keywords };

			char[] splitchars = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".ToCharArray();

			return keywords.Split(splitchars);
		}
*/	}

	/// <summary>
	/// Date matching options
	/// </summary>
	/// <remarks>
	/// This enumeration is used internally for date searches. The individual flags determine
	/// how two dates have to relate to each other to be a match.
	/// </remarks>
	public enum DateMatchType: 
		int
	{
		/// <summary>
		/// Earlier dates
		/// </summary>
		/// <remarks>
		/// Dates will match the criteria if they occur before the date searched for.
		/// </remarks>
		Before,
		/// <summary>
		/// Within date periods
		/// </summary>
		/// <remarks>
		/// Date periods will match the criteria if they include the date searched for.
		/// </remarks>
		Within,
		/// <summary>
		/// Later dates
		/// </summary>
		/// <remarks>
		/// Dates will match the criteria if they occur after the date searched for.
		/// </remarks>
		After
	}
		
	/// <summary>
	/// Date conditions
	/// </summary>
	/// <remarks>
	/// This class is used for date searches.
	/// </remarks>
	public class DateCondition:
		SearchCondition
	{
		/// <summary>
		/// Start date
		/// </summary>
		/// <remarks>
		/// The start date of a period, or the exact date.
		/// </remarks>
		private int start;
		/// <summary>
		/// End Date
		/// </summary>
		/// <remarks>
		/// The end date of a period. Dates match if they fall within the specified period.
		/// If a specific date is required, the end date is the same as the start date.
		/// The end date is always a later date than the start date. 
		/// </remarks>
		private int end;
		/// <summary>
		/// Comparison type
		/// </summary>
		/// <remarks>
		/// Dates can be matched with before, within or after comparisons
		/// </remarks>
		private DateMatchType match;

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Date searches operate on the FieldData table in the database.
		/// </remarks>
		public DateCondition()
		{
		}

		/// <summary>
		/// Creates a new DateCondition object from a XML representation
		/// </summary>
		/// <remarks>
		/// This constructor is the counter-part of the <see cref="DateCondition.ToXml(XmlDocument)"/> method.
		/// </remarks>
		/// <param name="node">The XmlNode containing the DateCondition information.</param>
		public DateCondition(XmlNode node):
			this()
		{
			field = new SearchField(node.FirstChild);
			XmlAttribute s = node.Attributes["start"];
			XmlAttribute e = node.Attributes["end"];
			start = (s == null ? Int32.MinValue : Int32.Parse(s.Value));
			end = (e == null ? Int32.MaxValue : Int32.Parse(e.Value));
			switch (node.Attributes["match"].Value)
			{
				case "before":
					match = DateMatchType.Before;
					break;
				case "after":
					match = DateMatchType.After;
					break;
				case "within":
					match = DateMatchType.Within;
					break;
				default:
					throw new CoreException("Invalid attribute value");
			}
		}

		/// <summary>
		/// Start date
		/// </summary>
		/// <remarks>
		/// If the start date is later than the end date, the end date
		/// is set to the start date.
		/// </remarks>
		/// <value>
		/// The start date of the period, or the exact date to search for.
		/// </value>
		public int Start
		{
			get
			{
				return start;
			}
			set
			{
				start = value;
				if (start > end)
					end = start;
			}
		}

		/// <summary>
		/// End date
		/// </summary>
		/// <remarks>
		/// Dates match if they fall within the specified period.
		/// If a specific date is required, the end date is the same as the start date.
		/// If the end date is before the start date, the start date
		/// is set to the end date.
		/// </remarks>
		/// <value>
		/// The end date of a period. 
		/// </value>
		public int End
		{
			get
			{
				return end;
			}
			set
			{
				end = value;
				if (start > end)
					start = end;
			}
		}

		/// <summary>
		/// Match type
		/// </summary>
		/// <remarks>
		/// This property can hold one of the possible
		/// <see cref="DateMatchType"/> values: after, before or within.
		/// </remarks>
		/// <value>
		/// The type of match this condition represents.
		/// </value>
		public DateMatchType Match
		{
			get
			{
				return match;
			}
			set
			{

				match = value;
			}
		}

		/// <summary>
		/// Create SQL WHERE statement to find matching dates
		/// </summary>
		/// <remarks>
		/// This method creates a SQL WHERE statement that can be used on the DateFieldsIndex
		/// table in the database to find records matching the date conditions of a search.
		/// </remarks>
		/// <returns>a string containing the WHERE clause for the search query</returns>
		internal override string WhereCondition()
		{
			if (match == DateMatchType.Before)
				return String.Format("(EndDate <= {0} AND EndDate > {1})", start, int.MinValue);
			if (match == DateMatchType.After)
				return String.Format("(StartDate >= {0})", end);
			if (match == DateMatchType.Within)
				return String.Format("(StartDate <= {0} AND StartDate > {2} AND EndDate >= {1})", 
					end, start, int.MinValue);
			// no match type set
			return "(0=1)";
		}

		internal override string GetLuceneCondition(Collection coll)
		{
			string condition;
			if (match == DateMatchType.Before)
				condition = String.Format("{{0}}__end:{{{0} TO {1}}}",
					FullTextIndex.Number.ToIndex(Int32.MinValue),
					FullTextIndex.Number.ToIndex(start));
			else if (match == DateMatchType.After)
				condition = String.Format("{{0}}__start:{{{0} TO {1}}}",
					FullTextIndex.Number.ToIndex(start),
					FullTextIndex.Number.ToIndex(Int32.MaxValue));
			else if (match == DateMatchType.Within)
				condition = String.Format("{{0}}__start:{{{0} TO {1}}} AND {{0}}__end:{{{2} TO {3}}}",
					FullTextIndex.Number.ToIndex(Int32.MinValue),
					FullTextIndex.Number.ToIndex(end),
					FullTextIndex.Number.ToIndex(start),
					FullTextIndex.Number.ToIndex(Int32.MaxValue));
			// no match type set
			else
				return null;
			return InsertFieldNamesInLuceneCondition(coll, condition);
		}

		/// <summary>
		/// The condition as an XML element
		/// </summary>
		/// <remarks>
		/// This method returns this condition as an XML element
		/// </remarks>
		/// <param name="doc">The document element in which the condition will be placed</param>
		/// <returns>An XmlElement object representing this condition</returns>
		internal override XmlElement ToXml(XmlDocument doc)
		{
			XmlElement e = doc.CreateElement("date");
			XmlAttribute a = doc.CreateAttribute("start");
			a.Value = start.ToString();
			e.Attributes.Append(a);
			a = doc.CreateAttribute("end");
			a.Value = end.ToString();
			e.Attributes.Append(a);
			a = doc.CreateAttribute("match");
			if (match == DateMatchType.Before)
				a.Value = "before";
			else if (match == DateMatchType.After)
				a.Value = "after";
			else if (match == DateMatchType.Within)
				a.Value = "within";
			e.Attributes.Append(a);
			e.AppendChild(field.ToXml(doc));
			return e;
		}
		
		/// <summary>
		/// Convert to string
		/// </summary>
		/// <remarks>
		/// This method returns a string representation of this search condition, useful
		/// for debugging purposes.
		/// </remarks>
		/// <returns>A string describing this search condition</returns>
		public override string ToString()
		{
			return String.Format("DateCondition (start={0} end={1} match={2})",
				start, end, match);
		}
	}
}
