using System;
using System.Data;
using System.Collections;
using System.Xml;
using System.Xml.Schema;
using System.IO;

namespace Orciid.Core
{
	/// <summary>
	/// Search Field
	/// </summary>
	/// <remarks>
	/// This class represents a field in connection with searches. It can hold either
	/// an internal field name or a dublin core element and refinement.
	/// </remarks>
	[Serializable()]
	public class SearchField:
		IComparable
	{
		/// <summary>
		/// An internal field name
		/// </summary>
		private string name;
		/// <summary>
		/// A dublin core element name
		/// </summary>
		private string dcelement;
		/// <summary>
		/// A dublin core refinement name
		/// </summary>
		private string dcrefinement;

		private string label;

		/// <summary>
		/// Default constructor
		/// </summary>
		/// <remarks>
		/// The default constructor for this class
		/// </remarks>
		public SearchField()
		{
		}

		/// <summary>
		/// Creates a new SearchField object from a XML representation
		/// </summary>
		/// <remarks>
		/// This constructor is the counter-part of the <see cref="SearchField.ToXml(XmlDocument)"/> method.
		/// </remarks>
		/// <param name="node">The XmlNode containing the SearchField information.</param>
		public SearchField(XmlNode node)
		{
			if (node == null || (node.Name != "field" && node.Name != "dcfield"))
				throw new CoreException("Invalid node");
			if (node.Attributes["label"] != null)
				label = node.Attributes["label"].Value;
			if (node.Attributes["name"] != null)
				name = node.Attributes["name"].Value;
			else if (node.Attributes["element"] != null)
			{
				dcelement = node.Attributes["element"].Value;
				if (node.Attributes["refinement"] != null)
					dcrefinement = node.Attributes["refinement"].Value;
			}
			else
				throw new CoreException("Invalid attribute list");
		}

		/// <summary>
		/// Create XML representation of this SearchField object
		/// </summary>
		/// <remarks>
		/// This method creates an XML representation of this object that
		/// can be stored and later be used to recreate an identical SearchField object.
		/// </remarks>
		/// <param name="doc">The XmlDocument the created XmlElement will be added to.</param>
		/// <returns>An XmlElement containing the information held in this object.</returns>
		public XmlElement ToXml(XmlDocument doc)
		{
			XmlElement e;
			if (IsInternal())
			{
				e = doc.CreateElement("field");
				XmlAttribute a = doc.CreateAttribute("name");
				a.Value = name;
				e.Attributes.Append(a);
			}
			else
			{
				e = doc.CreateElement("dcfield");
				XmlAttribute a = doc.CreateAttribute("element");
				a.Value = dcelement;
				e.Attributes.Append(a);
				if (dcrefinement != null)
				{
					a = doc.CreateAttribute("refinement");
					a.Value = dcrefinement;
					e.Attributes.Append(a);
				}
			}
			if (label != null)
			{
				XmlAttribute a = doc.CreateAttribute("label");
				a.Value = label;
				e.Attributes.Append(a);
			}
			return e;
		}

		/// <summary>
		/// Internal field name
		/// </summary>
		/// <remarks>
		/// When setting this property, the <see cref="DCElement"/> and <see cref="DCRefinement"/>
		/// properties are automatically set to <c>null</c>.
		/// </remarks>
		/// <value>
		/// The internal field name of a field.
		/// </value>
		public string Name
		{
			get
			{
				return name;
			}
			set
			{
				name = value;
				dcelement = null;
				dcrefinement = null;
			}
		}

		/// <summary>
		/// Field label
		/// </summary>
		/// <remarks>
		/// This is only relevant with regards to the user interface, it does not have any
		/// impact on the program.
		/// </remarks>
		/// <value>
		/// The label of the field.
		/// </value>
		public string Label
		{
			get
			{
				return label;
			}
			set
			{
				label = value;
			}
		}

		/// <summary>
		/// Dublin core element name
		/// </summary>
		/// <remarks>
		/// When setting this property, the <see cref="Name"/> property
		/// is automatically set to <c>null</c>.
		/// </remarks>
		/// <value>
		/// The dublin core element name of a field.
		/// </value>
		public string DCElement
		{
			get
			{
				return dcelement;
			}
			set
			{
				name = null;
				dcelement = value;
			}
		}

		/// <summary>
		/// Dublin core refinement name
		/// </summary>
		/// <remarks>
		/// When setting this property, the <see cref="Name"/> property
		/// is automatically set to <c>null</c>.
		/// </remarks>
		/// <value>
		/// The dublin core refinement name of a field.
		/// </value>
		public string DCRefinement
		{
			get
			{
				return dcrefinement;
			}
			set
			{
				name = null;
				dcrefinement = value;
			}
		}

		/// <summary>
		/// Check if representing internal name
		/// </summary>
		/// <remarks>
		/// Any <see cref="SearchField"/> can either represent an internal field name
		/// or a combination of dublin core element and refinement names.
		/// </remarks>
		/// <returns><c>true</c> if this object represents an internal field name, <c>false</c>
		/// otherwise.</returns>
		public bool IsInternal()
		{
			return (name != null);
		}

		/// <summary>
		/// Check if representing dublin core name
		/// </summary>
		/// <remarks>
		/// Any <see cref="SearchField"/> can either represent an internal field name
		/// or a combination of dublin core element and refinement names.
		/// </remarks>
		/// <returns><c>true</c> if this object represents a dublin core name, <c>false</c>
		/// otherwise.</returns>
		public bool IsDC()
		{
			return (dcelement != null);
		}

		/// <summary>
		/// Check if representing anything
		/// </summary>
		/// <remarks>
		/// Any <see cref="SearchField"/> can either represent an internal field name
		/// or a combination of dublin core element and refinement names.
		/// </remarks>
		/// <returns><c>true</c> if this object neither represents an internal field name, nor
		/// a dublin core name.</returns>
		public bool IsEmpty()
		{
			return (name == null && dcelement == null);
		}

		/// <summary>
		/// Retrieve field name
		/// </summary>
		/// <remarks>
		/// This method returns the name represented by this object. If it is representing 
		/// a dublin core refinement, the result will be in a dot-notation 
		/// (e.g. <c>coverage.spatial</c>).
		/// </remarks>
		/// <returns>The field name represented by this object, or <c>null</c> if it is empty.</returns>
		public string GetFieldName()
		{
			if (name != null)
				return name;
			if (dcelement != null)
				return dcelement + (dcrefinement != null ? "." + dcrefinement : "");
			return null;
		}

		/// <summary>
		/// Retrieve field label
		/// </summary>
		/// <remarks>
		/// This method returns the label of the field represented by this object.</remarks>
		/// <returns>The label of the field represented by this object, 
		/// or its field name if no label is set.</returns>
		public string GetFieldLabel()
		{
			if (label != null)
				return label;
			else
				return GetFieldName();
		}

		/// <summary>
		/// Check equality of two SearchFields
		/// </summary>
		/// <remarks>
		/// A SearchField works like a value type - if all three name properties are equal,
		/// the objects could as equal.
		/// </remarks>
		/// <param name="a">A SearchField object</param>
		/// <param name="b">Another SearchField object</param>
		/// <returns><c>true</c> if the two objects are equal, <c>false</c> otherwise.</returns>
		public static bool operator==(SearchField a, SearchField b)
		{
			if ((object)a == null || (object)b == null)
				return ((object)a == null && (object)b == null);
			return (a.name == b.name && 
				a.dcelement == b.dcelement && 
				a.dcrefinement == b.dcrefinement);
		}

		/// <summary>
		/// Check inequality of two SearchFields
		/// </summary>
		/// <remarks>
		/// A SearchField works like a value type - if all three name properties are equal,
		/// the objects could as equal.
		/// </remarks>
		/// <param name="a">A SearchField object</param>
		/// <param name="b">Another SearchField object</param>
		/// <returns><c>false</c> if the two objects are equal, <c>true</c> otherwise.</returns>
		public static bool operator!=(SearchField a, SearchField b)
		{
			return !(a == b);
		}

		/// <summary>
		/// Check equality with a SearchField
		/// </summary>
		/// <remarks>
		/// A SearchField works like a value type - if all three name properties are equal,
		/// the objects could as equal.
		/// </remarks>
		/// <param name="o">Another SearchField object</param>
		/// <returns><c>true</c> if the specified objects is equal to this object,
		/// <c>false</c> otherwise.</returns>
		public override bool Equals(object o)
		{
			return (o is SearchField && (SearchField)o == this);
		}

		/// <summary>
		/// Hash code
		/// </summary>
		/// <remarks>
		/// The hash code for a SearchField object is calculated as a combination of
		/// the hash codes of its three string properties.
		/// </remarks>
		/// <returns>A hash code for this object</returns>
		public override int GetHashCode()
		{
			if (name != null)
				return name.GetHashCode();
			if (dcelement != null)
				return (dcrefinement != null ? 
					dcelement.GetHashCode() ^ dcrefinement.GetHashCode() :
					dcelement.GetHashCode());
			if (dcrefinement != null)
				return dcrefinement.GetHashCode();
			return 0;
		}

		/// <summary>
		/// Compare two SearchFields
		/// </summary>
		/// <remarks>
		/// This method relies on <see cref="String.CompareTo(string)"/>. The properties are
		/// evaluated in the order <see cref="Name"/>, <see cref="DCElement"/>, 
		/// <see cref="DCRefinement"/>.
		/// </remarks>
		/// <param name="obj">Another SearchField object</param>
		/// <returns><c>-1</c> if this object precedes the given object when sorting,
		/// <c>0</c> if both objects are equal, <c>1</c> otherwise.</returns>
		public int CompareTo(object obj)
		{
			if (obj == null || !(obj is SearchField))
				throw new ArgumentException();
			SearchField f = (SearchField)obj;
			if (name != null)
				return name.CompareTo(f.name);
			if (f.name != null)
				return -1;
			if (dcelement != null)
			{
				int c = dcelement.CompareTo(f.dcelement);
				if (c == 0)
				{
					if (dcrefinement != null)
						return dcrefinement.CompareTo(f.dcrefinement);
					return (f.dcrefinement != null) ? -1 : 0;
				}
				else
					return c;
			}
			return (f.dcelement != null) ? -1 : 0;
		}

	}

    /// <summary>
    /// Search engine enumeration
    /// </summary>
    /// <remarks>
    /// List of available search engines
    /// </remarks>
	public enum SearchEngine
	{
        /// <summary>
        /// Default
        /// </summary>
        /// <remarks>
        /// Refers to the default search engine.  Only use if it does not
        /// matter which search engine is used.
        /// </remarks>
		Default, 
        /// <summary>
        /// Internal database search engine
        /// </summary>
        /// <remarks>
        /// Uses the internal full-text index of the database
        /// </remarks>
        Database, 
        /// <summary>
        /// Lucene search engine
        /// </summary>
        /// <remarks>
        /// Uses the Lucene full-text index
        /// </remarks>
        Lucene
	}

	/// <summary>
	/// Search collections
	/// </summary>
	/// <remarks>
	/// The Search class is used to perform searches on the catalog. To run a search,
	/// create a Search object, set the criteria, and execute the search to receive
	/// an array of identifiers of matching images. Every search object can only be
	/// validated and executed once. Even after a failed validation, it has to be
	/// discarded and re-built.
	/// </remarks>
	public class Search
	{
		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// The default constructor
		/// </remarks>
		public Search()
		{
		}

		/// <summary>
		/// Stores parameters for a search
		/// </summary>
		/// <remarks>
		/// This class combines several ArrayLists holding information about
		/// the parameters for a search.
		/// </remarks>
		public class SearchParameters: ICloneable
		{
			/// <summary>
			/// Warnings
			/// </summary>
			/// <remarks>
			/// An ArrayList of strings holding user readable warnings.
			/// </remarks>
			public ArrayList warnings = new ArrayList();
			/// <summary>
			/// Collections to search
			/// </summary>
			/// <remarks>
			/// An ArrayList of Collection objects to include in the search.
			/// </remarks>
			public ArrayList collections = new ArrayList();
			/// <summary>
			/// Search conditions
			/// </summary>
			/// <remarks>
			/// An ArrayList of SearchCondition objects to use in the search.
			/// </remarks>
			public ArrayList conditions = new ArrayList();
            /// <summary>
            /// The <see cref="SearchEngine"/> to use when running searches
            /// </summary>
            /// <remarks>
            /// Initially set to <see cref="Orciid.Core.SearchEngine.Default"/>
            /// </remarks>
			public SearchEngine SearchEngine = SearchEngine.Default;

			/// <summary>
			/// Create XML representation of this search parameters object
			/// </summary>
			/// <remarks>
			/// This method creates an XML representation of the parameters for a
			/// search. The XML can then be passed on or stored e.g. in a database and
			/// later be used to recreate and rerun an identical search.
			/// </remarks>
			/// <returns>A string containing the XML representation of this object.</returns>
			public string ToXml()
			{
				XmlDocument doc = new XmlDocument();
				XmlNode root = doc.CreateElement("search");
				doc.AppendChild(root);
				XmlElement colelem = doc.CreateElement("collections");
				root.AppendChild(colelem);
				if (collections.Count == 0)
					colelem.AppendChild(doc.CreateElement("all"));
				else
					foreach (Collection collection in collections)
					{
						XmlElement c = doc.CreateElement("collection");
						XmlAttribute a = doc.CreateAttribute("id");
						a.Value = collection.ID.ToString();
						c.Attributes.Append(a);
						colelem.AppendChild(c);
					}
				XmlElement conelem = doc.CreateElement("conditions");
				root.AppendChild(conelem);
				foreach (SearchCondition searchcon in conditions)
				{
					XmlElement e = searchcon.ToXml(doc);
					if (e != null)
						conelem.AppendChild(e);
				}
				return doc.OuterXml;
			}

			/// <summary>
			/// Default constructor
			/// </summary>
			/// <remarks>
			/// The default constructor for this class
			/// </remarks>
			public SearchParameters()
			{
			}

			#region SavedSearchXSD
			private const string SavedSearchXSD = @"
<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema' elementFormDefault='qualified' attributeFormDefault='unqualified'>
	<xs:element name='search'>
		<xs:complexType>
			<xs:sequence>
				<xs:element name='collections'>
					<xs:complexType>
						<xs:choice>
							<xs:element name='collection' maxOccurs='unbounded'>
								<xs:complexType>
									<xs:attribute name='id' type='xs:integer' use='required'/>
								</xs:complexType>
							</xs:element>
							<xs:element name='all'>
								<xs:complexType/>
							</xs:element>
						</xs:choice>
					</xs:complexType>
				</xs:element>
				<xs:element name='conditions'>
					<xs:complexType>
						<xs:sequence>
							<xs:choice maxOccurs='unbounded'>
								<xs:element name='keyword'>
									<xs:complexType>
										<xs:attribute name='text' type='xs:string' use='required'/>
									</xs:complexType>
								</xs:element>
								<xs:element name='text'>
									<xs:complexType>
										<xs:choice>
											<xs:element ref='field'/>
											<xs:element ref='dcfield'/>
										</xs:choice>
										<xs:attribute name='text' type='xs:string' use='required'/>
										<xs:attribute name='exact' type='xs:string' use='optional'/>
										<xs:attribute name='comparefullvalue' type='xs:string' use='optional'/>
									</xs:complexType>
								</xs:element>
								<xs:element name='date'>
									<xs:complexType>
										<xs:choice>
											<xs:element ref='field'/>
											<xs:element ref='dcfield'/>
										</xs:choice>
										<xs:attribute name='match' use='required'>
											<xs:simpleType>
												<xs:restriction base='xs:string'>
													<xs:enumeration value='before'/>
													<xs:enumeration value='within'/>
													<xs:enumeration value='after'/>
												</xs:restriction>
											</xs:simpleType>
										</xs:attribute>
										<xs:attribute name='start' type='xs:integer' use='optional'/>
										<xs:attribute name='end' type='xs:integer' use='optional'/>
									</xs:complexType>
								</xs:element>
								<xs:element name='recorddate'>
									<xs:complexType>
										<xs:sequence>
											<xs:choice minOccurs='0'>
												<xs:sequence>
													<xs:element name='createdafter' type='xs:dateTime' minOccurs='0'/>
													<xs:element name='createdbefore' type='xs:dateTime' minOccurs='0'/>
												</xs:sequence>
												<xs:element name='createdwithin' type='xs:integer'/>
											</xs:choice>
											<xs:choice minOccurs='0'>
												<xs:sequence>
													<xs:element name='modifiedafter' type='xs:dateTime' minOccurs='0'/>
													<xs:element name='modifiedbefore' type='xs:dateTime' minOccurs='0'/>
												</xs:sequence>
												<xs:element name='modifiedwithin' type='xs:integer'/>
											</xs:choice>
										</xs:sequence>
									</xs:complexType>
								</xs:element>
								<xs:element name='owner'>
									<xs:complexType>
										<xs:attribute name='public' use='required'>
											<xs:simpleType>
												<xs:restriction base='xs:string'>
													<xs:enumeration value='yes'/>
													<xs:enumeration value='no'/>
												</xs:restriction>
											</xs:simpleType>
										</xs:attribute>
										<xs:attribute name='own' use='required'>
											<xs:simpleType>
												<xs:restriction base='xs:string'>
													<xs:enumeration value='yes'/>
													<xs:enumeration value='no'/>
												</xs:restriction>
											</xs:simpleType>
										</xs:attribute>
										<xs:attribute name='others' use='required'>
											<xs:simpleType>
												<xs:restriction base='xs:string'>
													<xs:enumeration value='yes'/>
													<xs:enumeration value='no'/>
												</xs:restriction>
											</xs:simpleType>
										</xs:attribute>
									</xs:complexType>
								</xs:element>
							</xs:choice>
						</xs:sequence>
					</xs:complexType>
				</xs:element>
			</xs:sequence>
		</xs:complexType>
	</xs:element>
	<xs:element name='field'>
		<xs:complexType>
			<xs:attribute name='name' type='xs:string' use='required'/>
		</xs:complexType>
	</xs:element>
	<xs:element name='dcfield'>
		<xs:complexType>
			<xs:attribute name='element' type='xs:string' use='required'/>
			<xs:attribute name='refinement' type='xs:string' use='optional'/>
		</xs:complexType>
	</xs:element>
</xs:schema>
";
			#endregion

			/// <summary>
			/// Creates a SearchParameters object from XML
			/// </summary>
			/// <remarks>This constructor recreates a SearchParameters object that has
			/// previously been converted to XML using the <see cref="ToXml"/> method.</remarks>
			/// <param name="xml">The XML representation of a SearchParameters object as a string</param>
			public SearchParameters(string xml)
			{
				XmlValidatingReader reader = new XmlValidatingReader(xml, XmlNodeType.Document, null);
				XmlSchemaCollection schemas = new XmlSchemaCollection();
				schemas.Add(null, new XmlTextReader(new StringReader(SavedSearchXSD)));
				reader.ValidationType = ValidationType.Schema;
				reader.Schemas.Add(schemas);
				XmlDocument doc = new XmlDocument();
				doc.Load(reader);
				XmlNodeList nodes = doc.SelectNodes("/search/collections/*");
				foreach (XmlNode node in nodes)
				{
					if (node.Name == "collection")
					{
						Collection c = Collection.GetByID(Int32.Parse(node.Attributes["id"].Value));
						if (c != null)
							collections.Add(c);
					}
				}
				nodes = doc.SelectNodes("/search/conditions/*");
				foreach (XmlNode node in nodes)
					conditions.Add(SearchCondition.FromXml(node));
			}
			#region ICloneable Members

			/// <summary>
			/// Clone search parameters
			/// </summary>
			/// <remarks>
			/// This method clones a SearchParameters object by creating a new
			/// object and cloning all contained collections
			/// </remarks>
			/// <returns>A clone of this object</returns>
			public object Clone()
			{
				SearchParameters sp = new SearchParameters();
				sp.collections = (ArrayList)collections.Clone();
				sp.conditions = (ArrayList)conditions.Clone();
				sp.warnings = (ArrayList)warnings.Clone();
				return sp;
			}

			#endregion
		}

		/// <summary>
		/// Execute a search
		/// </summary>
		/// <remarks>
		/// This method executes a search by applying the given search parameters
		/// to the specified collections.
		/// </remarks>
		/// <param name="parameters">The parameters of the search</param>
		/// <param name="maxresults">The maximum number of matching results to return.
		/// If more matches are found, an undetermined subset is returned.
		/// To return all results, use <see cref="Int32.MaxValue"/>.</param>
		/// <param name="truncateresults">Set to <c>true</c> to truncate search result list 
		/// if more results than allowed are found.  If set to <c>false</c> the search will fail
		/// if too many results are found.</param>
		/// <returns>An array of image identifiers that fulfill the requirements of
		/// the search parameters, or <c>null</c> if no images could be found for any reason.</returns>
		public static ImageIdentifier[] Execute(SearchParameters parameters, int maxresults, bool truncateresults)
		{
			return Execute(parameters, maxresults, truncateresults, null);
		}

        /// <summary>
        /// Execute a search
        /// </summary>
        /// <remarks>
        /// This method executes a search by applying the given search parameters
        /// to the specified collections.
        /// </remarks>
        /// <param name="parameters">The parameters of the search</param>
        /// <param name="maxresults">The maximum number of matching results to return.
        /// If more matches are found, an undetermined subset is returned.
        /// To return all results, use <see cref="Int32.MaxValue"/>.</param>
        /// <param name="truncateresults">Set to <c>true</c> to truncate search result list 
        /// if more results than allowed are found.  If set to <c>false</c> the search will fail
        /// if too many results are found.</param>
        /// <param name="searchwithin">An array of ImageIdentifiers that the search is restricted to</param>
        /// <returns>An array of image identifiers that fulfill the requirements of
        /// the search parameters, or <c>null</c> if no images could be found for any reason.</returns>
		public static ImageIdentifier[] Execute(SearchParameters parameters, int maxresults, bool truncateresults, ImageIdentifier[] searchwithin)
		{		
			if (parameters == null ||
				parameters.conditions == null || parameters.conditions.Count == 0)
				return null;
			ArrayList colls = new ArrayList(parameters.collections);
			if (colls.Count == 0)
				return null;
			// use a hashtable for the combined result since the same image might be found
			// in more than one collection, but the combined result should not have duplicates
			Hashtable combined = new Hashtable();
			SearchCondition[] conditions = new SearchCondition[parameters.conditions.Count];
			parameters.conditions.CopyTo(conditions);
			foreach (Collection coll in colls)
			{
				if (coll != null)
				{
					ImageIdentifier[] images = coll.Search(conditions, parameters.SearchEngine, searchwithin);
					if (images != null)
						foreach (ImageIdentifier i in images)
							if (!combined.ContainsKey(i))
								combined.Add(i, null);
				}
			}
			ImageIdentifier[] result;
			if (combined.Count <= maxresults)
			{
				result = new ImageIdentifier[combined.Count];
				combined.Keys.CopyTo(result, 0);
			}
			else
			{
				if (!truncateresults)
					throw new TooManySearchResultsException("Too many images found");

				result = new ImageIdentifier[maxresults];
				int copied = 0;
				foreach (ImageIdentifier i in combined.Keys)
				{
					result[copied] = i;
					copied++;
					if (copied >= maxresults)
						break;
				}
			}
			return result;
		}
		
		/// <summary>
		/// Sort list of internal image identifiers by field values 
		/// </summary>
		/// <remarks>
		/// This method sorts a list of internal image identifiers by the values for a specified
		/// field. Since this is cross-collection, the field has to be specified by its internal
		/// name. If some images do not have a field value for the specified field, they are
		/// unsorted at the end of the list. This method always sorts alphabetically by the display
		/// value, except if all fields are date fields, which are sorted by their internal 
		/// start date (if available). If an image record has multiple values for a field, only the 
		/// first one is considered when sorting. Display values are sorted by the first 255 characters.
		/// </remarks>
		/// <param name="field">The field to sort by</param>
		/// <param name="images">A list of image identifiers to sort</param>
		/// <returns>A list of image identifiers sorted by field values</returns>
		public static ImageIdentifier[] Sort(SearchField field, ImageIdentifier[] images)
		{
			if (images == null || images.Length == 0 || field == null || field.IsEmpty()) 
				return images;
			ArrayList cids = new ArrayList();
			Hashtable given = new Hashtable(images.Length);
			ArrayList leftover = new ArrayList();
			ArrayList result = new ArrayList(images.Length);
			foreach (ImageIdentifier i in images)
			{
				// handle duplicate image entries
				if (!given.ContainsKey(i))
					given.Add(i, null);
				else
					leftover.Add(i);
				if (!cids.Contains(i.CollectionID))
					cids.Add(i.CollectionID);
			}
			// hashtable with collections as keys and fields to sort on as values
//			Hashtable collections = new Hashtable(); 
//			ArrayList wherestatements = new ArrayList();
			ArrayList fieldids = new ArrayList();
			bool alldates = true;
			foreach (int cid in cids)
			{
				Collection coll = Collection.GetByID(cid);
				if (coll != null)
				{
					Field f = coll.GetField(field.GetFieldName(), true, field.IsDC());
					if (f != null)
					{
//						collections.Add(coll, f);
						fieldids.Add(f.ID);
						alldates = alldates && f.Type == FieldType.Date;						
					}
				}
			}

			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT ImageID,CollectionID FROM FieldData 
						WHERE FieldInstance=0 AND FieldID IN {fieldids}
						ORDER BY {@orderby}");
				query.AddParam("fieldids", fieldids);
				query.AddParam("@orderby", 
					alldates ? "StartDate" : conn.SortableTextField("FieldValue"));
				using (IDataReader reader = conn.ExecReader(query))
				{
					while (reader.Read())
					{
						ImageIdentifier id = new ImageIdentifier(
							conn.DataToInt(reader.GetValue(0), 0),
							conn.DataToInt(reader.GetValue(1), 0));
						if (given.ContainsKey(id))
						{
							result.Add(id);
							given.Remove(id);
						}
					}
				}
			}

			result.AddRange(given.Keys);
			result.AddRange(leftover);
			return (ImageIdentifier[])result.ToArray(typeof(ImageIdentifier));

/*

			ArrayList given = new ArrayList(images);			// the list of images to sort
			ArrayList result = new ArrayList(images.Length);	// the sorted list of the same images
			Hashtable collimages = Collection.SeperateImageIdsByCollection(images);
			string[] wherestatements = new string[collimages.Count];
			int w = 0;
			foreach (int cid in collimages.Keys)
			{
				ArrayList a = (ArrayList)collimages[cid];
				Collection coll = Collection.GetByID(cid);
				if (coll != null)
				{
					string[] s = new string[a.Count];
					for (int i = 0; i < a.Count; i++)
						s[i] = ((ImageIdentifier)a[i]).ID.ToString();
					wherestatements[w++] = String.Format(
						"(FieldID={0} AND ImageID IN ({1}))",
						((Field)collections[coll]).ID, String.Join(",", s));
				}
				else
				{
					// invalid collection, just make up a term that evaluates to false
					wherestatements[w++] = "(0=1)";
				}	
			}
			using (DBConnection conn = DBConnector.GetConnection())
			{
				DataTable table;
				Query query = new Query(conn,
					@"SELECT ImageID,CollectionID FROM FieldData 
						WHERE FieldInstance=0 AND ({@where}) ORDER BY {@orderby}");
				query.AddParam("@where", String.Join(" OR ", wherestatements));
				query.AddParam("@orderby", alldates ? "StartDate" : conn.SortableTextField("FieldValue"));
				table = conn.SelectQuery(query);
				foreach (DataRow row in table.Rows)
				{
					ImageIdentifier id = new ImageIdentifier(
						conn.DataToInt(row["ImageID"], 0),
						conn.DataToInt(row["CollectionID"], 0));
					given.Remove(id);
					result.Add(id);
				}
			}
			// add remaining (unsortable) images to result
			result.AddRange(given);
			return (ImageIdentifier[])result.ToArray(typeof(ImageIdentifier));

*/

		}

		/// <summary>
		/// Find fields common to different collections
		/// </summary>
		/// <remarks>
		/// This method finds and returns an array of common fields in a given set
		/// of collections. If only one collection is specified, the returned 
		/// <see cref="SearchField"/> objects represent internal field names, if there
		/// are two or more collections, dublin core elements and refinements are used instead.
		/// For a field to be returned, there must be a field with the same name in every
		/// given collection. All fields also must have the appropriate flags for searching
		/// and sorting set as required by the given parameters in order to be included in
		/// the result.
		/// </remarks>
		/// <param name="collections">An array of collections</param>
		/// <param name="keywordsearchable">If this flag is <c>true</c>, only keyword searchable
		/// fields are included in the result.</param>
		/// <param name="searchable">If this flag is <c>true</c>, only searchable fields
		/// are included in the result.</param>
		/// <param name="sortable">If this flag is <c>true</c>, only sortable fields
		/// are included in the result.</param>
		/// <returns>An array of common fields, or <c>null</c> if no common fields
		/// are found or no collections were specified.</returns>
		public static SearchField[] CommonFields(Collection[] collections,
			bool keywordsearchable, bool searchable, bool sortable)
		{
			if (collections == null || collections.Length == 0)
				return null;
			ArrayList result = new ArrayList();
			if (collections.Length == 1)
			{
				foreach (Field field in collections[0].GetFields())
					if ((!keywordsearchable || field.KeywordSearchable) &&
						(!searchable || field.Searchable) &&
						(!sortable || field.Sortable))
					{
						SearchField s = new SearchField();
						s.Name = field.Name;
						s.Label = field.Label;
						result.Add(s);
					}
			}
			else
			{
				// use first collection as default set of fields
				foreach (Field field in collections[0].GetFields())
					if ((!keywordsearchable || field.KeywordSearchable) &&
						(!searchable || field.Searchable) &&
						(!sortable || field.Sortable) && 
						field.DCElement != null)
					{
						SearchField s = new SearchField();
						s.Label = field.Label;
						s.DCElement = field.DCElement;
						s.DCRefinement = field.DCRefinement;
						result.Add(s);
					}
				// now loop through all other collections
				for (int idx = 1; idx < collections.Length && result.Count > 0; idx++)
					if (collections[idx] != null)
					{
						bool match = false;
						// every previously found field must exist in this collection as well
						for (int ridx = result.Count - 1; ridx >= 0; ridx--)
						{
							SearchField s = (SearchField)result[ridx];
							// find the current field
							foreach (Field field in collections[idx].GetFields())
								if ((!keywordsearchable || field.KeywordSearchable) &&
									(!searchable || field.Searchable) &&
									(!sortable || field.Sortable) && 
									String.Compare(field.DCElement, s.DCElement, true) == 0 && 
									String.Compare(field.DCRefinement, s.DCRefinement, true) ==0)
								{
									match = true;
									if (s.Label.IndexOf(field.Label) < 0)
										s.Label += "/" + field.Label;
									break;
								}
							// if field not found, remove it from result
							if (!match)
								result.RemoveAt(ridx);
						}
					}
				// whatever fields are left in result are common to all collections
			}
			if (result.Count == 0)
				return null;
			SearchField[] r = new SearchField[result.Count];
			result.CopyTo(r);
			return r;
		}

		/// <summary>
		/// Find fields common to different collections
		/// </summary>
		/// <remarks>
		/// This method finds and returns an array of common fields for a set
		/// of images. If only images from one collection is specified, the returned 
		/// <see cref="SearchField"/> objects represent internal field names, if there
		/// are images from two or more collections, dublin core elements and refinements are used instead.
		/// For a field to be returned, there must be a field with the same name in every
		/// collection that is represented by at least one of the given images. 
		/// All fields also must have the appropriate flags for searching
		/// and sorting set as required by the given parameters in order to be included in
		/// the result.
		/// </remarks>
		/// <param name="images">An array of images</param>
		/// <param name="keywordsearchable">If this flag is <c>true</c>, only keyword searchable
		/// fields are included in the result.</param>
		/// <param name="searchable">If this flag is <c>true</c>, only searchable fields
		/// are included in the result.</param>
		/// <param name="sortable">If this flag is <c>true</c>, only sortable fields
		/// are included in the result.</param>
		/// <returns>An array of common fields, or <c>null</c> if no common fields
		/// are found or no images were specified.</returns>
		public static SearchField[] CommonFields(ImageIdentifier[] images,
			bool keywordsearchable, bool searchable, bool sortable)
		{
			if (images == null)
				return null;
			Hashtable h = new Hashtable();
			int count = 0;
			foreach (ImageIdentifier id in images)
				if (!h.ContainsKey(id.CollectionID))
				{
					Collection c = Collection.GetByID(id.CollectionID);
					h.Add(id.CollectionID, c);
					if (c != null)
						count++;
				}
			if (count == 0)
				return null;
			Collection[] collections = new Collection[count];
			foreach (Collection c in h.Values)
				if (c != null)
				{
					count--;
					collections[count] = c;
				}
			return CommonFields(collections, keywordsearchable, searchable, sortable);
		}
	}	
}
