using System;
using System.Collections;
using System.Data;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.Xml;
using System.Xml.Schema;
using System.Text.RegularExpressions;
using Orciid.Media;
using Orciid.Media.Util;
using Orciid.Media.Converters;
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;
using Orciid.Core.Util;

namespace Orciid.Core
{	
	/// <summary>
	/// Available collection types
	/// </summary>
	/// <remarks>
	/// Every collection must be assigned a collection type. Usually, this will be <c>Internal</c>
	/// for regular collections. <c>Favorites</c> and <c>Personal</c> are two special
	/// collection types implemented for search purposes.
	/// </remarks>
	public enum CollectionType:
		int
	{
		/// <summary>
		/// A regular internal collection
		/// </summary>
		Internal,
		/// <summary>
		/// A virtual collection mapping a user's favorite images
		/// </summary>
		Favorites,
		/// <summary>
		/// A virtual collection mapping a user's personal images
		/// </summary>
		PersonalImages,
		/// <summary>
		/// A virtual collection mapping other user's shared images
		/// </summary>
		SharedImages,
		/// <summary>
		/// A remote collection
		/// </summary>
		Remote,
		/// <summary>
		/// A special collection of some other type
		/// </summary>
		Other,
	}

	/// <summary>
	/// Image caching options
	/// </summary>
	/// <remarks>
	/// Controls what image sizes can be delivered to uncontrolled clients and 
	/// how images are cached by controlled clients
	/// </remarks>
	public enum ImageCaching:
		int
	{
		/// <summary>
		/// Cache all images
		/// </summary>
		/// <remarks>
		/// All images sizes may be cached - use this if there are no restrictions on use
		/// of full sized images.
		/// </remarks>
		All,
		/// <summary>
		/// Cache only medium sized images
		/// </summary>
		/// <remarks>
		/// Only medium sized images may be cached - use this if there are restrictions on
		/// the full sized images.
		/// </remarks>
		Medium,
		/// <summary>
		/// Do not cache images
		/// </summary>
		/// <remarks>
		/// No images are cached (with the exception of thumbnails) - use this if there
		/// are restrictions on full and medium sized images.
		/// </remarks>
		None
	}

	internal class CollectionFactory
	{
		public static Collection Create(char type)
		{
			switch (type)
			{
				case 'I':
					return new Collection(false);
				case 'F':
					return new FavoritesCollection(false);
				case 'P':
					return new PersonalImagesCollection(false);
				case 'S':
					return new SharedImagesCollection(false);
				case 'R':
					return new RemoteCollection(false);
				case 'N':
					return new NIXRemoteCollection(false);
				default:
					throw new CoreException("Unsupported collection type");
			}
		}
	}

	/// <summary>
	/// A collection of images within the system.
	/// </summary>
	/// <remarks>Every collection has its own catalog, the <see cref="Field"/> meta data.
	/// Within the documentation for this project, the terms collection and catalog are interchangeable
	/// and refer to a collection.
	/// </remarks>
	public class Collection:
		CachableObject, IAccessControlled, IComparable, IHasProperties
	{
		/// <summary>
		/// The class identifier, used for caching
		/// </summary>
		protected const string classid = "Orciid.Core.Collection";
		/// <summary>
		/// The internal identifier of the collection
		/// </summary>
		protected int id;
		/// <summary>
		/// The title of the collection
		/// </summary>
		protected string title;
		/// <summary>
		/// The description of the collection.
		/// </summary>
		protected string description;
		/// <summary>
		/// The usage agreement for this collection.
		/// </summary>
		protected string usageagreement;
		/// <summary>
		/// Path to image resources
		/// </summary>
		protected ResourcePath resourcepath;
		/// <summary>
		/// The <see cref="Field"/> meta data for the catalog of this collection
		/// </summary>
		protected ArrayList fields = new ArrayList();
		/// <summary>
		/// The access control object for this collection
		/// </summary>
		protected AccessControl accesscontrol = null;
		/// <summary>
		/// Settings for full sized images
		/// </summary>
		protected ImageSettings fullImageSettings;
		/// <summary>
		/// Settings for medium sized images
		/// </summary>
		protected ImageSettings mediumImageSettings;
		/// <summary>
		/// Settings for thumbnail sized images
		/// </summary>
		protected ImageSettings thumbImageSettings;
		/// <summary>
		/// Image cache restrictions
		/// </summary>
		protected ImageCaching caching = ImageCaching.None;
		/// <summary>
		/// Collection group membership
		/// </summary>
		protected int group;

		/// <summary>
		/// Creates a new collection
		/// </summary>
		/// <remarks>
		/// To create a new collection, the current <see cref="User"/> needs to have sufficient
		/// privileges.
		/// </remarks>
		public Collection():
			base()
		{
			accesscontrol = new AccessControl(this);
		}

		/// <summary>
		/// Creates a new collection without initializing parameters or checking privileges
		/// </summary>
		/// <remarks>
		/// This constructor is called when a collection is loaded from the database or
		/// otherwise created without being a new collection in the system.
		/// </remarks>
		/// <param name="init">dummy parameter, should be set to <c>false</c>. This is required
		/// to distinguish it from the public constructor.</param>
		internal Collection(bool init):
			base(init)
		{
		}

		/// <summary>
		/// Compares the titles of two collections
		/// </summary>
		/// <remarks>
		/// This method is implemented for the IComparable interface. It allows collections
		/// to be sorted by their title.
		/// </remarks>
		/// <param name="c">Another collection to compare this collection's title to</param>
		/// <returns>The result of the comparison of the collections' titles</returns>
		public int CompareTo(object c)
		{
			return title.CompareTo(((Collection)c).title);
		}

		/// <summary>
		/// Load list of collection identifiers
		/// </summary>
		/// <remarks>
		/// This method loads the complete list of collection identifiers from the database,
		/// regardless of the type of the collections or accessibility by the current user.
		/// </remarks>
		/// <returns>An arraylist of all collection identifiers in the system</returns>
		public static ArrayList GetCollectionIDs()
		{
			ArrayList ids = GetIDsFromCache(classid);
			if (ids == null)
			{
				ids = new ArrayList();
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						"SELECT ID FROM Collections");
					DataTable table = conn.SelectQuery(query);
					foreach (DataRow row in table.Rows)
						ids.Add(conn.DataToInt(row["ID"], 0));
				}
				AddIDsToCache(classid, ids);
			}
			return ids;
		}
		
		/// <summary>
		/// Retrieve collection by its internal identifier
		/// </summary>
		/// <remarks>
		/// This method returns the specified collection.
		/// </remarks>
		/// <param name="id">The identifier of the collection to retrieve</param>
		/// <returns>The collection matching the specified identifier, or <c>null</c> if no
		/// collection with that identifier exists or the current <see cref="User"/> does not
		/// have the required privileges to read the collection</returns>
		public static Collection GetByID(int id) 
		{
			return GetByID(true, id);
		}

		internal static Collection GetByID(bool enforcepriv, int id)
		{
			Collection c = (Collection)GetFromCache(classid, id);
			if (c == null)
			{
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"SELECT ID,Type,Title,Description,ResourcePath,UsageAgreement,
						FullImageHeight,FullImageWidth,FullImageQuality,FullImageFixedSize,FullImageBGColor,
						MediumImageHeight,MediumImageWidth,MediumImageQuality,MediumImageFixedSize,MediumImageBGColor,
						ThumbImageHeight,ThumbImageWidth,ThumbImageQuality,ThumbImageFixedSize,ThumbImageBGColor,
						GroupID,CacheRestriction 
						FROM Collections WHERE ID={id}");
					query.AddParam("id", id);
					DataTable table = conn.SelectQuery(query);
					if (table.Rows.Count == 1)
					{
						DataRow row = table.Rows[0];
						c = CollectionFactory.Create(conn.DataToChar(row["Type"], '\0'));
						c.id = conn.DataToInt(row["ID"], 0);
						c.title = conn.DataToString(row["Title"]);
						c.description = conn.DataToString(row["Description"]);
						c.resourcepath = new ResourcePath(conn.DataToString(row["ResourcePath"]));
						c.usageagreement = conn.DataToString(row["UsageAgreement"]);
						c.fullImageSettings.height = conn.DataToInt(row["FullImageHeight"], 3000);
						c.fullImageSettings.width = conn.DataToInt(row["FullImageWidth"], 3000);
						c.fullImageSettings.quality = conn.DataToInt(row["FullImageQuality"], 97);
						c.fullImageSettings.fixedsize = conn.DataToBool(row["FullImageFixedSize"], false);
						c.fullImageSettings.backgroundcolor = conn.DataToInt(row["FullImageBGColor"], 0);
						c.mediumImageSettings.height = conn.DataToInt(row["MediumImageHeight"], 800);
						c.mediumImageSettings.width = conn.DataToInt(row["MediumImageWidth"], 800);
						c.mediumImageSettings.quality = conn.DataToInt(row["MediumImageQuality"], 97);
						c.mediumImageSettings.fixedsize = conn.DataToBool(row["MediumImageFixedSize"], false);
						c.mediumImageSettings.backgroundcolor = conn.DataToInt(row["MediumImageBGColor"], 0);
						c.thumbImageSettings.height = conn.DataToInt(row["ThumbImageHeight"], 72);
						c.thumbImageSettings.width = conn.DataToInt(row["ThumbImageWidth"], 96);
						c.thumbImageSettings.quality = conn.DataToInt(row["ThumbImageQuality"], 97);
						c.thumbImageSettings.fixedsize = conn.DataToBool(row["ThumbImageFixedSize"], true);
						c.thumbImageSettings.backgroundcolor = conn.DataToInt(row["ThumbImageBGColor"], 0);
						c.group = conn.DataToInt(row["GroupID"], 0);
						c.caching = (ImageCaching)conn.DataToInt(row["CacheRestriction"], (int)ImageCaching.None);
						c.fields = Field.GetForCollection(c.id);
						c.accesscontrol = new AccessControl(c);
						c.InitializeAdditionalFields(conn);
						AddToCache(c);
					}
				}
			}
			if (c == null || (enforcepriv && !User.HasPrivilege(Privilege.ReadCollection, c)))
				return null;
			else
				return c;
		}

		/// <summary>
		/// Object initializer
		/// </summary>
		/// <remarks>
		/// This method is called after the collection object is retrieved from the database.
		/// It allows any subclasses to retrieve any other information from the database before
		/// the object will be used. Override this method when necessary.  In the collection base
		/// class, this method does not do anything.
		/// </remarks>
		/// <param name="conn">An open database connection</param>
		protected virtual void InitializeAdditionalFields(DBConnection conn)
		{
		}

		/// <summary>
		/// Return available collections
		/// </summary>
		/// <remarks>
		/// Changes to the array itself do not have any effect on the collections in the system,
		/// whereas changes to the collection objects will be reflected. 
		/// Only collections the current <see cref="User"/> has access
		/// to are returned.
		/// </remarks>
		/// <returns>an array of collections, sorted by their title. The array is empty if the
		/// current <see cref="User"/> does not have access to any collection.</returns>
		public static Collection[] GetCollections()
		{
			ArrayList ids = GetCollectionIDs();
			ArrayList authorized = new ArrayList();
			foreach (int id in ids)
			{
				Collection c = GetByID(id);
				if (c != null && User.HasPrivilege(Privilege.ReadCollection, c))
					authorized.Add(c);
			}
			Collection[] colls = new Collection[authorized.Count];
			authorized.CopyTo(colls, 0);
			Array.Sort(colls);
			return colls;
		}

		/// <summary>
		/// Return available collections of a certain type
		/// </summary>
		/// <remarks>
		/// Changes to the array itself do not have any effect on the collections in the system,
		/// whereas changes to the collection objects will be reflected. 
		/// Only collections matching the specified type and that the current 
		/// <see cref="User"/> has access to are returned.
		/// </remarks>
		/// <param name="type">The type of collections to return</param>
		/// <returns>an array of collections, sorted by their title. The array is empty if the
		/// current <see cref="User"/> does not have access to any collection of
		/// the specified type.</returns>
		public static Collection[] GetCollections(params CollectionType[] type)
		{
			ArrayList ids = GetCollectionIDs();
			ArrayList authorized = new ArrayList();
			foreach (int id in ids)
			{
				Collection c = GetByID(id);
				if (c != null && 
					User.HasPrivilege(Privilege.ReadCollection, c))
					foreach (CollectionType t in type)
						if (t == c.Type)
						{
							authorized.Add(c);
							break;
						}
			}
			if (authorized.Count == 0)
				return new Collection[0];
			Collection[] colls = new Collection[authorized.Count];
			authorized.CopyTo(colls, 0);
			Array.Sort(colls);
			return colls;			
		}

		/// <summary>
		/// Removes the collection from the system.
		/// </summary>
		/// <remarks>
		/// Removing a collection from the system will immediately delete all data from the database,
		/// making the deletion permanent.
		/// </remarks>
		public void Delete()
		{
			User.RequirePrivilege(Privilege.DeleteCollection, this);
			MarkModified();
			accesscontrol.Clear();
			Properties.ClearProperties(this);
			CollectionShareEntry.DeleteAll(this.id);
			foreach (ControlledList list in ControlledList.GetControlledLists(this.id))
				list.Delete(true);
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"DELETE FROM FieldData WHERE CollectionID={id}");
				query.AddParam("id", id);
				conn.ExecQuery(query);

				query = new Query(conn,
					@"DELETE FROM Images WHERE CollectionID={id}");
				query.AddParam("id", id);
				conn.ExecQuery(query);

				query = new Query(conn,
					@"DELETE FROM FieldDefinitions WHERE CollectionID={id}");
				query.AddParam("id", id);
				conn.ExecQuery(query);

				query = new Query(conn,
					@"DELETE FROM Collections WHERE ID={id}");
				query.AddParam("id", id);
				conn.ExecQuery(query);
			}
			RemoveFromCache(this, true);
			id = 0;			
			ResetModified();
		}

		/// <summary>
		/// Check modification privilege
		/// </summary>
		/// <remarks>
		/// This method checks if a specified user has sufficient privileges to modify this object.
		/// </remarks>
		/// <param name="user">The user to check privileges for</param>
		/// <returns><c>true</c> if the User has sufficient privileges to modify this object, 
		/// <c>false</c> otherwise.</returns>
		public override bool CanModify(User user)
		{
			return User.HasPrivilege(Privilege.ManageCollection, this, user);
		}

		/// <summary>
		/// Check for ModifyACL privilege
		/// </summary>
		/// <remarks>
		/// Calls <see cref="User.HasPrivilege(Privilege, IAccessControlled, User)" /> to check for <see cref="Privilege.ModifyACL"/>.
		/// </remarks>
		/// <param name="clearing">Set to <c>true</c> if the ACL is being cleared because
		/// the related object is being deleted, this eases access restrictions for
		/// modifying the ACL</param>
		/// <param name="user">The user to check the privilege for</param>
		/// <returns><c>true</c> if the specified user is allowed to modify the access control
		/// list for this object, <c>false</c> otherwise.</returns>
		public bool CanModifyACL(User user, bool clearing)
		{
			return User.HasPrivilege(Privilege.ModifyACL, this, user);
		}

		/// <summary>
		/// Check creation privilege
		/// </summary>
		/// <remarks>
		/// This method checks if a specified user has sufficient privileges to create this object.
		/// This method is called from the constructor, so the object already exists and therefore CanCreate
		/// does not need to be static.
		/// </remarks>
		/// <param name="user">The user to check privileges for</param>
		/// <returns><c>true</c> if the User has sufficient privileges to create this object, 
		/// <c>false</c> otherwise.</returns>
		public override bool CanCreate(User user)
		{
			return User.HasPrivilege(Privilege.CreateCollection, user);
		}

		/// <summary>
		/// Check image modification privilege
		/// </summary>
		/// <remarks>
		/// This method checks if the specified user has sufficient privileges
		/// to modify the given image.
		/// </remarks>
		/// <param name="user">The user to check privileges for</param>
		/// <param name="image">The image to be modified</param>
		/// <returns><c>true</c> if the User has sufficient privileges to modify the image, 
		/// <c>false</c> otherwise.</returns>
		public virtual bool CanModifyImage(User user, Image image)
		{
			return User.HasPrivilege(Privilege.ModifyImages, this, user) ||
				(user != null && 
				User.HasPrivilege(Privilege.PersonalImages, this, user) && 
				image.userid == user.ID);
		}

		/// <summary>
		/// Check image creation privilege
		/// </summary>
		/// <remarks>
		/// This method checks if the specified user has sufficient privileges
		/// to create an image.
		/// </remarks>
		/// <param name="user">The user to check privileges for</param>
		/// <param name="personal"><c>true</c> if a personal image should be created,
		/// <c>false</c> if a regular image for the collection should be created</param>
		/// <returns><c>true</c> if the User has sufficient privileges to create the image, 
		/// <c>false</c> otherwise.</returns>
		public virtual bool CanCreateImage(User user, bool personal)
		{
			if (personal)
			{
				return User.HasPrivilege(Privilege.PersonalImages, this, user);
				// FogBugz case 142: check for maximum number of allowed images here
			}
			else
			{
				return User.HasPrivilege(Privilege.ModifyImages, this, user);
			}
		}

		/// <summary>
		/// The title of the collection
		/// </summary>
		/// <remarks>
		/// A collection's title must always be specified.
		/// </remarks>
		/// <value>The title of the collection</value>
		/// <exception cref="CoreException">Thrown if the assigned value is <c>null</c> or empty.</exception>
		public string Title
		{
			get
			{
				return title;
			}
			set
			{
				if (value != null && value.Length > 0) 
				{
					MarkModified();
					title = (value.Length > 50 ? value.Substring(0, 50) : value);
				}
				else
					throw new CoreException("Title must not be null or empty.");
			}
		}

		/// <summary>
		/// The title of the collection
		/// </summary>
		/// <remarks>
		/// The title can also be retrieved using the <see cref="Title"/> property.
		/// </remarks>
		/// <returns>The title of the collection</returns>
		public string GetTitle()
		{
			return title;
		}

		/// <summary>
		/// The description of the collection
		/// </summary>
		/// <remarks>
		/// A collection's description must always be specified.
		/// </remarks>
		/// <value>The description of the collection</value>
		/// <exception cref="CoreException">Thrown if the assigned value is <c>null</c> or empty.</exception>
		public string Description
		{
			get
			{
				return description;
			}
			set
			{
				if (value != null && value.Length > 0) 
				{
					MarkModified();
					description = value;
				}
				else
					throw new CoreException("Description must not be null or empty.");
			}
		}

		/// <summary>
		/// The usage agreement for the collection
		/// </summary>
		/// <remarks>
		/// Every collection may have a usage agreement associated with it. 
		/// Note: There is no built-in enforcement of acceptance of the usage agreement. 
		/// There is no method to reverse accepting the agreement. Every time this 
		/// property is changed, all recorded acceptances are removed.
		/// </remarks>
		/// <value>The usage agreement for the collection</value>
		public string UsageAgreement
		{
			get
			{
				return usageagreement;
			}
			set
			{
				MarkModified();
				User user = User.CurrentUser();
				usageagreement = (value != null && value.Length > 0) ? value : null;
				if (id > 0)
					using (DBConnection conn = DBConnector.GetConnection())
					{
						Query query = new Query(conn,
							@"DELETE FROM TrackUsageAgreements WHERE CollectionID={id}");
						query.AddParam("id", id);
						conn.ExecQuery(query);
					}
				TransactionLog.Instance.Add("Usage Agreement Updated",
					String.Format("{0} ({1})\n{2}", 
					(user != null ? user.Login : "No active user"),
					(user != null ? user.ID : 0),
					(usageagreement != null ? usageagreement : "[Agreement removed]")));
			}
		}

		/// <summary>
		/// User accepts usage agreements
		/// </summary>
		/// <remarks>
		/// Every collection may have a usage agreement associated with it. If this is the case,
		/// acceptance of that agreement can be recorded with this method.
		/// Note: There is no built-in enforcement of acceptance of the usage agreement. 
		/// There is no method to reverse accepting the agreement. Once the agreement is modified,
		/// all recorded acceptances are removed.
		/// </remarks>
		/// <param name="user">The User accepting the usage agreement</param>
		public void AcceptUsageAgreement(User user)
		{
			// don't store accepted user agreements for anonymous user (user.ID < 0)
			if (id > 0 && user.ID > 0 && usageagreement != null)
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"DELETE FROM TrackUsageAgreements 
						WHERE CollectionID={collectionid} AND UserID={userid}");
					query.AddParam("collectionid", id);
					query.AddParam("userid", user.ID);
					conn.ExecQuery(query);

					query = new Query(conn,
						@"INSERT INTO TrackUsageAgreements (CollectionID,UserID,DateAccepted) 
						VALUES ({collectionid},{userid},{dateaccepted})");
					query.AddParam("collectionid", id);
					query.AddParam("userid", user.ID);
					query.AddParam("dateaccepted", DateTime.Now);
					conn.ExecQuery(query);
				}
		}

		/// <summary>
		/// Checks usage agreement acceptance
		/// </summary>
		/// <remarks>
		/// Every collection may have a usage agreement associated with it. If this is the case,
		/// acceptance of that agreement can be checked with this method. If the collection does
		/// not have a usage agreement, this method always returns <c>true</c>.
		/// Note: There is no built-in enforcement of acceptance of the usage agreement. 
		/// There is no method to reverse accepting the agreement. Once the agreement is modified,
		/// all recorded acceptances are removed.
		/// </remarks>
		/// <param name="user">The User to check</param>
		/// <returns><c>true</c> if the user accepted the agreement or if there is no
		/// agreement, <c>false</c> otherwise</returns>
		public bool HasAcceptedUsageAgreement(User user)
		{
			if (usageagreement == null)
				return true;
			if (id > 0 && user.ID > 0)
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"SELECT COUNT(*) FROM TrackUsageAgreements 
						WHERE CollectionID={collectionid} AND UserID={userid}");
					query.AddParam("collectionid", id);
					query.AddParam("userid", user.ID);
					return conn.DataToInt(conn.ExecScalar(query), 0) > 0;
				}
			else
				return false;
		}

		/// <summary>
		/// The path to image resources
		/// </summary>
		/// <remarks>
		/// A physical path to the image file resources for this collection.
		/// This must point to a directory on a local hard drive or network share. 
		/// Certain placeholders are allowed.
		/// </remarks>
		/// <value>The path to image resources for this collection</value>
		/// <exception cref="CoreException">Thrown if the assigned value is <c>null</c> or empty.</exception>
		public string ResourcePath
		{
			get
			{
				return resourcepath.ToString();
			}
			set
			{
				if (value != null && value.Length > 0) 
				{
					MarkModified();
					if (value.Length > 255)
						throw new CoreException("Maximum resource path length is 255 characters");
					resourcepath = new ResourcePath(value);
					//					if (resourcepath[resourcepath.Length - 1] != '\\')
					//						resourcepath += @"\";
				}
				else
					throw new CoreException("ResourcePath must not be null or empty.");
			}
		}

		/// <summary>
		/// Physical path to image resources
		/// </summary>
		/// <remarks>
		/// This property returns a physical path to a directory under which all image
		/// resources for this collection are stored. Any placeholders in the <see cref="ResourcePath"/>
		/// are excluded.
		/// </remarks>
		/// <value>
		/// The physical path to the image resources, or an empty string if no physical path
		/// exists.
		/// </value>
		public string PhysicalResourcePathRoot
		{
			get
			{
				return resourcepath.PhysicalRoot;
			}
		}

		/// <summary>
		/// Get the number of images in collection
		/// </summary>
		/// <remarks>
		/// This method returns the number of images in the collection.  This does no
		/// longer include personal or shared images, only public images.
		/// </remarks>
		/// <value>The number of public images</value>
		public virtual int ImageCount
		{
			get
			{
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"SELECT COUNT(*) FROM Images WHERE CollectionID={collectionid} 
						AND ((UserID=0) OR (UserID IS NULL))");
					query.AddParam("collectionid", id);
					return conn.DataToInt(conn.ExecScalar(query), 0);
				}
			}
		}

		/// <summary>
		/// Writes a modified collection object to the database
		/// </summary>
		/// <remarks>After writing the collection to the database, the object is returned
		/// to an unmodifiable state.
		/// </remarks>
		/// <exception cref="CoreException">Thrown if the object cannot be written
		/// to the database.</exception>
		/// <exception cref="CoreException">Thrown if the title, description or resource path for
		/// the collection are not set.</exception>
		protected override void CommitChanges()
		{
			if (title == null || title.Length == 0 || description == null || description.Length == 0
				|| resourcepath == null) // || resourcepath.Length == 0)
				throw new CoreException("Title, Description and ResourcePath have to be set for a Collection");
			int result;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query;
				if (id == 0) 
				{
					query = new Query(conn,
						@"INSERT INTO Collections 
						(Title,Description,ResourcePath,UsageAgreement,Type,
						FullImageHeight,FullImageWidth,FullImageQuality,FullImageFixedSize,FullImageBGColor,
						MediumImageHeight,MediumImageWidth,MediumImageQuality,MediumImageFixedSize,MediumImageBGColor,
						ThumbImageHeight,ThumbImageWidth,ThumbImageQuality,ThumbImageFixedSize,ThumbImageBGColor,
						GroupID,CacheRestriction) 
						VALUES ({title},{description},{resourcepath},{usageagreement},{type},
						{fullimageheight},{fullimagewidth},{fullimagequality},{fullimagefixedsize},{fullimagebgcolor},
						{mediumimageheight},{mediumimagewidth},{mediumimagequality},{mediumimagefixedsize},{mediumimagebgcolor},
						{thumbimageheight},{thumbimagewidth},{thumbimagequality},{thumbimagefixedsize},{thumbimagebgcolor},
						{groupid},{cacherestriction})");
					query.AddParam("title", title);
					query.AddParam("description", description);
					query.AddParam("resourcepath", resourcepath.ToString());
					query.AddParam("usageagreement", usageagreement);
					query.AddParam("type", 'I');
					query.AddParam("fullimageheight", fullImageSettings.height);
					query.AddParam("fullimagewidth", fullImageSettings.width);
					query.AddParam("fullimagequality", fullImageSettings.quality);
					query.AddParam("fullimagefixedsize", fullImageSettings.fixedsize);
					query.AddParam("fullimagebgcolor", fullImageSettings.backgroundcolor);
					query.AddParam("mediumimageheight", mediumImageSettings.height);
					query.AddParam("mediumimagewidth", mediumImageSettings.width);
					query.AddParam("mediumimagequality", mediumImageSettings.quality);
					query.AddParam("mediumimagefixedsize", mediumImageSettings.fixedsize);
					query.AddParam("mediumimagebgcolor", mediumImageSettings.backgroundcolor);
					query.AddParam("thumbimageheight", thumbImageSettings.height);
					query.AddParam("thumbimagewidth", thumbImageSettings.width);
					query.AddParam("thumbimagequality", thumbImageSettings.quality);
					query.AddParam("thumbimagefixedsize", thumbImageSettings.fixedsize);
					query.AddParam("thumbimagebgcolor", thumbImageSettings.backgroundcolor);
					query.AddParam("groupid", group);
					query.AddParam("cacherestriction", caching);
					result = conn.ExecQuery(query);
					if (result == 1)
					{
						id = conn.LastIdentity("Collections");
						accesscontrol.GenerateDefaultPrivileges();
						AddToCache(this, true);
					}
					else
						throw new CoreException("Could not write new collection object to database");
				}
				else
				{
					query = new Query(conn,
						@"UPDATE Collections 
						SET Title={title},Description={description},ResourcePath={resourcepath},
						UsageAgreement={usageagreement},
						FullImageHeight={fullimageheight},FullImageWidth={fullimagewidth},
						FullImageQuality={fullimagequality},FullImageFixedSize={fullimagefixedsize},
						FullImageBGColor={fullimagebgcolor},
						MediumImageHeight={mediumimageheight},MediumImageWidth={mediumimagewidth},
						MediumImageQuality={mediumimagequality},MediumImageFixedSize={mediumimagefixedsize},
						MediumImageBGColor={mediumimagebgcolor},
						ThumbImageHeight={thumbimageheight},ThumbImageWidth={thumbimagewidth},
						ThumbImageQuality={thumbimagequality},ThumbImageFixedSize={thumbimagefixedsize},
						ThumbImageBGColor={thumbimagebgcolor},
						GroupID={groupid},CacheRestriction={cacherestriction} 
						WHERE ID={id}");
					query.AddParam("title", title);
					query.AddParam("description", description);
					query.AddParam("resourcepath", resourcepath.ToString());
					query.AddParam("usageagreement", usageagreement);
					query.AddParam("fullimageheight", fullImageSettings.height);
					query.AddParam("fullimagewidth", fullImageSettings.width);
					query.AddParam("fullimagequality", fullImageSettings.quality);
					query.AddParam("fullimagefixedsize", fullImageSettings.fixedsize);
					query.AddParam("fullimagebgcolor", fullImageSettings.backgroundcolor);
					query.AddParam("mediumimageheight", mediumImageSettings.height);
					query.AddParam("mediumimagewidth", mediumImageSettings.width);
					query.AddParam("mediumimagequality", mediumImageSettings.quality);
					query.AddParam("mediumimagefixedsize", mediumImageSettings.fixedsize);
					query.AddParam("mediumimagebgcolor", mediumImageSettings.backgroundcolor);
					query.AddParam("thumbimageheight", thumbImageSettings.height);
					query.AddParam("thumbimagewidth", thumbImageSettings.width);
					query.AddParam("thumbimagequality", thumbImageSettings.quality);
					query.AddParam("thumbimagefixedsize", thumbImageSettings.fixedsize);
					query.AddParam("thumbimagebgcolor", thumbImageSettings.backgroundcolor);
					query.AddParam("groupid", group);
					query.AddParam("cacherestriction", caching);
					query.AddParam("id", id);
					result = conn.ExecQuery(query);
				}
			}
			if (result != 1) 
				throw new CoreException("Could not write modified collection object to database");
			resourcepath.CreateImageDirectories();
		}

		/// <summary>
		/// returns the <see cref="Field"/>s for this collection
		/// </summary>
		/// <remarks>
		/// This function returns the meta data of this collection as an array of <see cref="Field"/>s. 
		/// The array is sorted in the same order as the fields appear in the collection.
		/// </remarks>
		/// <returns>an array of fields for this collection</returns>
		public virtual Field[] GetFields()
		{
			return (Field[])fields.ToArray(typeof(Field));
		}

		/// <summary>
		/// returns an individual <see cref="Field"/> from this collection
		/// </summary>
		/// <remarks>
		/// Use <see cref="GetFields()"/> to retrieve a complete list of fields in this collection.
		/// </remarks>
		/// <param name="id">The internal identifier of the <see cref="Field"/></param>
		/// <returns>The <see cref="Field"/> object or <c>null</c> if the identifier was not found
		/// in this collection</returns>
		public Field GetField(int id)
		{
			foreach (Field f in GetFields())
				if (f.ID == id)
					return f;
			return null;
		}

		/// <summary>
		/// returns an individual <see cref="Field"/> matching the specified name from this collection
		/// </summary>
		/// <remarks>
		/// Use <see cref="GetFields()"/> to retrieve a complete list of fields in this collection.
		/// This method returns the first field with a matching name, even if there are multiple
		/// matches.
		/// </remarks>
		/// <param name="name">The name of the requested field</param>
		/// <param name="ignorecase">Specifies if the textual comparison of the specified name to
		/// the field names should be case sensitive or not</param>
		/// <param name="dc">If this flag is true, the specified name is compared to
		/// the Dublin Core Element and/or Refinement instead of the internal field name</param>
		/// <returns>The requested field, or <c>null</c> if no matching field was found</returns>
		public Field GetField(string name, bool ignorecase, bool dc)
		{
			foreach (Field f in GetFields())
				if (f.IsNameMatch(name, ignorecase, dc))
					return f;
			return null;
		}

		/// <summary>
		/// returns an individual <see cref="Field"/> matching the specified name from this collection
		/// </summary>
		/// <remarks>
		/// This method returns the first field with a matching name, even if there are multiple
		/// matches.  The name is case sensitive.  Only internal names are checked (no Dublin Core
		/// field names).
		/// </remarks>
		/// <param name="name">The internal, case sensitive name of the requested field</param>
		/// <returns>The matching field, or <c>null</c> if no such field was found</returns>
		public Field GetField(string name)
		{
			return GetField(name, false, false);
		}

		/// <summary>
		/// returns all <see cref="Field"/>s matching the specified name from this collection
		/// </summary>
		/// <remarks>
		/// Use <see cref="GetFields()"/> to retrieve a complete list of fields in this collection.
		/// This method returns all fields with a matching name, in the order in which they are
		/// in the collection.
		/// </remarks>
		/// <param name="name">The name of the requested fields</param>
		/// <param name="ignorecase">Specifies if the textual comparison of the specified name to
		/// the field names should be case sensitive or not</param>
		/// <param name="dc">If this flag is true, the specified name is compared to
		/// the Dublin Core Element and/or Refinement instead of the internal field name</param>
		/// <returns>The requested fields, or <c>null</c> if no matching fields were found</returns>
		public Field[] GetFields(string name, bool ignorecase, bool dc)
		{
			ArrayList r = new ArrayList();
			foreach (Field f in GetFields())
				if (f.IsNameMatch(name, ignorecase, dc))
					r.Add(f);
			if (r.Count == 0)
				return null;
			Field[] result = new Field[r.Count];
			r.CopyTo(result);
			return result;
		}

		/// <summary>
		/// Retrieve image objects by internal identifiers
		/// </summary>
		/// <remarks>
		/// This method queries the database and returns image records.  The returned records
		/// are not sorted and may be in a different order than the image identifiers passed
		/// to the method.
		/// </remarks>
		/// <param name="enforcepriv">Specifies if collection access privileges will be
		/// enforced.  If <c>true</c>, this method will return <c>null</c> if the current 
		/// user does not have read privileges.</param>
		/// <param name="imageids">An ArrayList of image identifiers</param>
		/// <returns>An ArrayList of <see cref="Image"/> objects, or <c>null</c> if no
		/// image identifiers were passed or the collection is not accessible</returns>
		public virtual ArrayList GetImagesByID(bool enforcepriv, ArrayList imageids)
		{
			if (imageids == null || imageids.Count == 0 ||
				(enforcepriv && !User.HasPrivilege(Privilege.ReadCollection, this)))
				return null;
			ArrayList result = new ArrayList();
			User user = User.CurrentUser();
			bool curator = (user != null && User.HasPrivilege(Privilege.ModifyImages, this));
			using (DBConnection conn = DBConnector.GetConnection())
			{
				ArrayList ids = new ArrayList();
				foreach (ImageIdentifier i in imageids)
					if (i.CollectionID == this.id)
						ids.Add(i.ID.ToString());
				if (ids.Count == 0)
					return null;
				string wherecond = String.Format("(#CID#={0} AND #IID# IN ({1}))", 
					this.id, String.Join(",", (string[])ids.ToArray(typeof(String))));
				// Get two DataTables with all the necessary information to build an image record
				// (Images, FieldData).
				// Then go through the tables at the same time, reading as many rows as necessary.
				// This allows for building a lot of image records with only two database queries.
				Query query = new Query(conn,
					@"SELECT I.ID,I.UserID,I.Resource,I.Created,I.Modified,I.Flags,
					I.CachedUntil,I.Expires,I.RemoteID,I.RecordStatus,I.MimeType,U.Name,U.FirstName 
					FROM Images AS I LEFT JOIN Users AS U ON I.UserID=U.ID 
					WHERE ({@where}) ORDER BY I.ID");
				query.AddParam("@where", wherecond.Replace("#IID#", "I.ID").Replace("#CID#", "I.CollectionID"));
				DataTable images = conn.SelectQuery(query);

				query = new Query(conn,
					@"SELECT A.ImageID,A.FieldID,A.FieldValue,A.ControlledListValue,
					A.StartDate,A.EndDate,A.OriginalValue 
					FROM FieldData AS A 
					WHERE ({@where}) ORDER BY A.ImageID,A.FieldID,A.FieldInstance");
				query.AddParam("@where", wherecond.Replace("#IID#", "A.ImageID").Replace("#CID#", "A.CollectionID"));
				DataTable fields = conn.SelectQuery(query);

				if (images.Rows.Count >= 1) 
				{
					int fieldidx = 0;
					// idx is row pointer in Images table
					for (int idx = 0; idx < images.Rows.Count; idx++)
					{
						DataRow row = images.Rows[idx];
						int iid = conn.DataToInt(row["ID"], 0);
						Image image = new Image(new ImageIdentifier(iid, this.id), conn.DataToString(row["Resource"]));
						image.createddate = conn.DataToDateTime(row["Created"]);
						image.modifieddate = conn.DataToDateTime(row["Modified"]);
						image.cacheduntil = conn.DataToDateTime(row["CachedUntil"]);
						image.expires = conn.DataToDateTime(row["Expires"]);
						image.remoteid = conn.DataToString(row["RemoteID"]);
						image.recordstatus = (ImageRecordStatus)conn.DataToInt(row["RecordStatus"], 0);
						image.mimetype = conn.DataToString(row["MimeType"]);
						image.flags = (ImageFlag)conn.DataToInt(row["Flags"], 0);
						image.userid = conn.DataToInt(row["UserID"], 0);

						if (image.userid != 0)
						{
							string firstname = conn.DataToString(row["FirstName"]);
							string lastname = conn.DataToString(row["Name"]);
							image.username = User.GenerateFullName(lastname, firstname);
						}

						Hashtable data = new Hashtable();
						// this will walk through records in the FieldData and DateFieldsIndex tables
						// until no more rows for the current image record exist
						while (fieldidx < fields.Rows.Count && 
							conn.DataToInt(fields.Rows[fieldidx]["ImageID"], 0) == iid)
						{
							DataRow fieldrow = fields.Rows[fieldidx];
							int fieldid = conn.DataToInt(fieldrow["FieldID"], 0);
							if (!data.ContainsKey(fieldid))
								data.Add(fieldid, FieldData.CreateFieldData(image, GetField(fieldid)));
							((FieldData)data[fieldid]).Add(conn, fieldrow);
							fieldidx++;
						}
						image.fielddata = data;
						if (!enforcepriv || 
							image.userid == 0 ||
							((image.flags & ImageFlag.Shared) == ImageFlag.Shared) ||
							(curator && (image.flags & ImageFlag.InclusionSuggested) == ImageFlag.InclusionSuggested) ||
							(user != null && image.userid == user.ID))
							result.Add(image);
					}
				}
			}
			if (result.Count == 0)
				return null;
			else
				return result;
		}

		/// <summary>
		/// Retrieve image objects by resource
		/// </summary>
		/// <remarks>
		/// This utility method allows image retrieval by the resource information associated
		/// with each image.  Since image resources are not necessarily unique, more than one
		/// image may be returned for a particular resource.
		/// </remarks>
		/// <param name="resource">The resource to search for</param>
		/// <returns>An ArrayList of <see cref="Image"/> objects, or <c>null</c> if no
		/// images were found, no resources were specified, or the current user does not
		/// have read access to the collection.</returns>
		public virtual ArrayList GetImagesByResource(params string[] resource)
		{
			if (resource == null || resource.Length == 0 ||
				!User.HasPrivilege(Privilege.ReadCollection, this))
				return null;
			int[] ids = null;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT ID FROM Images 
					WHERE CollectionID={collectionid} AND Resource IN {resources}");
				query.AddParam("collectionid", this.id);
				query.AddParam("resources", resource);
				ids = conn.TableToArray(conn.SelectQuery(query));
			}
			if (ids != null && ids.Length > 0)
			{
				ArrayList imageids = new ArrayList(ids.Length);
				foreach (int id in ids)
					imageids.Add(new ImageIdentifier(id, this.id));
				return GetImagesByID(true, imageids);
			}
			return null;
		}

		/// <summary>
		/// Query distinct field values
		/// </summary>
		/// <remarks>
		/// This method supports collection browsing by returning distinct field values
		/// </remarks>
		/// <param name="field">The field for which to retrieve values</param>
		/// <param name="ascending">return values in ascending or descending order</param>
		/// <param name="count">number of values to return</param>
		/// <param name="include">Include or exclude the <c>startat</c> value</param>
		/// <param name="startat">The start value</param>
		/// <returns>A sorted ArrayList of strings containing the field values.</returns>
		public virtual string[] GetBrowseValues
			(Field field, string startat, bool include, bool ascending, int count)
		{
			if (field == null || field.CollectionID != id || field.ID == 0)
				return null;
			return GetBrowseValuesQuery(
				field, 
				startat, 
				include, 
				ascending, 
				count, 
				"",
				String.Format("FieldData.CollectionID={0}", id),
				String.Format("FieldID={0}", field.ID),
				"(Images.UserID=0 OR Images.UserID IS NULL)"
				);
		}

		/// <summary>
		/// Query distinct field values
		/// </summary>
		/// <remarks>
		/// This method should not be called directly, call <see cref="GetBrowseValues"/> 
		/// instead.
		/// </remarks>
		/// <param name="ascending">return values in ascending or descending order</param>
		/// <param name="count">number of values to return</param>
		/// <param name="field">The field for which to retrieve values</param>
		/// <param name="include">Include or exclude the <c>startat</c> value</param>
		/// <param name="joins">additional JOINs for the SQL query</param>
		/// <param name="startat">The start value</param>
		/// <param name="wheres">additional WHERE conditions for the SQL query</param>
		/// <returns>A sorted ArrayList of strings containing the field values.</returns>
		protected virtual string[] GetBrowseValuesQuery
			(Field field, string startat, bool include, bool ascending, int count,
			string joins, params string[] wheres)
		{
			if (field == null || !field.Browsable)
				return null;
			ArrayList values = new ArrayList();
			using (DBConnection conn = DBConnector.GetConnection())
			{
				string comparison = ascending ? ">" : "<";
				if (include) 
					comparison += "=";
				Query query = new Query(conn,
					@"SELECT DISTINCT TOP {count} {@FieldValue} AS V FROM FieldData 
					INNER JOIN Images ON 
					(FieldData.CollectionID=Images.CollectionID AND FieldData.ImageID=Images.ID)
					{@joins}
					WHERE ({@FieldValue} {@comparison} {startat}) AND {@wheres}
					ORDER BY {@FieldValue} {@direction}");
				query.AddParam("startat", startat);
				query.AddParam("count", count);
				query.AddParam("@FieldValue", conn.SortableTextField("FieldValue"));
				query.AddParam("@comparison", comparison);
				query.AddParam("@direction", ascending ? "" : "DESC");
				query.AddParam("@joins", joins);
				query.AddParam("@wheres", String.Join(" AND ", wheres));
				using (IDataReader reader = conn.ExecReader(query))
					while (reader.Read())
						values.Add(conn.DataToString(reader.GetValue(0)));
				if (!ascending)
					values.Reverse();
			}
			if (values.Count == 0)
				return null;
			else
				return (string[])values.ToArray(typeof(string));
		}

		/// <summary>
		/// Get personal images
		/// </summary>
		/// <remarks>
		/// This method returns the internal identifiers of all personal images for the
		/// current user.
		/// </remarks>
		/// <returns>An array of internal image identifiers</returns>
		public ImageIdentifier[] GetPersonalImages()
		{
			User user = User.CurrentUser();
			if (user == null)
				return null;
			int[] r = null;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT ID FROM Images WHERE CollectionID={collectionid} AND UserID={userid}");
				query.AddParam("collectionid", this.id);
				query.AddParam("userid", user.ID);
				r = conn.TableToArray(conn.SelectQuery(query));
			}
			if (r == null)
				return null;
			ImageIdentifier[] result = new ImageIdentifier[r.Length];
			for (int i = 0; i < r.Length; i++)
				result[i] = new ImageIdentifier(r[i], id);
			return result;
		}

		/// <summary>
		/// Image Navigation
		/// </summary>
		/// <remarks>
		/// Indicates if images in the collection can be navigated by going
		/// forward/backward between images.
		/// </remarks>
		/// <returns><c>true</c></returns>
		public virtual bool HasImageNavigation()
		{
			return true;
		}

		/// <summary>
		/// Find adjacent image
		/// </summary>
		/// <remarks>
		/// If the collection allows image navigation (see <see cref="HasImageNavigation"/>), this
		/// method returns the identifier of the image following or preceding the specified image.  
		/// To determine the order of images, the field values for the specified fields are sorted 
		/// alphabetically.
		/// </remarks>
		/// <param name="imageid">The identifier of the current image</param>
		/// <param name="fieldid">The field identifier to sort images by</param>
		/// <param name="fieldvalue">The value of the field to sort by of the current image</param>
		/// <param name="forward">Determines if the following or preceding image should be found</param>
		/// <param name="personalonly">If <c>true</c>, only images belonging to the current 
		/// user are searched, otherwise regular images are searched as well.</param>
		/// <returns>The identifier of the next image, or an empty image identifier (with both
		/// collection and image ID set to <c>0</c>) if no such image exists</returns>
		public virtual ImageIdentifier GetAdjacentImage
			(ImageIdentifier imageid, int fieldid, string fieldvalue, bool forward, bool personalonly)
		{
			if (!HasImageNavigation() ||
				imageid.CollectionID != this.id || 
				fieldvalue == null || fieldvalue.Length == 0)
				return new ImageIdentifier(0, 0);
			int id;
			User user = User.CurrentUser();
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT TOP 1 ImageID FROM FieldData INNER JOIN Images 
					ON (FieldData.ImageID=Images.ID AND FieldData.CollectionID=Images.CollectionID) 
					WHERE FieldData.CollectionID={collectionid} 
					AND FieldData.FieldID={fieldid} AND FieldData.FieldInstance=0 
					AND FieldData.FieldValue IS NOT NULL 
					AND ({@FieldValue}{@comp}{fieldvalue} 
						OR ({@FieldValue}={fieldvalue} AND FieldData.ImageID{@comp}{imageid})) 
					AND (Images.UserID={userid}" +
					(personalonly ? "" : " OR Images.UserID IS NULL OR Images.UserID=0") +
					@") 
					ORDER BY {@FieldValue}{@desc},FieldData.ImageID{@desc}");
				query.AddParam("collectionid", this.id);
				query.AddParam("fieldid", fieldid);
				query.AddParam("@FieldValue", conn.SortableTextField("FieldData.FieldValue"));
				query.AddParam("fieldvalue", fieldvalue);
				query.AddParam("imageid", imageid.ID);
				query.AddParam("@comp", forward ? ">" : "<");
				query.AddParam("@desc", forward ? "" : " DESC");
				query.AddParam("userid", user != null ? user.ID : 0);
				DataTable table = conn.SelectQuery(query);
				if (table.Rows.Count != 1)
					return new ImageIdentifier(0, 0);
				id = conn.DataToInt(table.Rows[0]["ImageID"], 0);
			}
			return new ImageIdentifier(id, this.id);
		}

		/// <summary>
		/// Delete an image
		/// </summary>
		/// <remarks>
		/// This method removes an image from the system.  The image object should no longer
		/// be used after calling this method.
		/// </remarks>
		/// <param name="image">The image to remove</param>
		public virtual void DeleteImage(Image image)
		{
			DeleteImage(true, image);
		}

		internal virtual void DeleteImage(bool enforcepriv, Image image)
		{
			if (image.CollectionID != id)
				throw new CoreException("Image to be deleted is not in this collection");
			if (enforcepriv)
			{
				if (image.userid == 0)
					User.RequirePrivilege(Privilege.ModifyImages, this);
				else
				{
					User.RequirePrivilege(Privilege.PersonalImages, this);
					User user = User.CurrentUser();
					if (user == null || user.ID != image.userid)
						throw new CoreException("Cannot delete personal image");
				}
			}
			bool removeresource = false;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"DELETE FROM FieldData 
					WHERE ImageID={imageid} AND CollectionID={collectionid}");
				query.AddParam("imageid", image.ID.ID);
				query.AddParam("collectionid", id);
				conn.ExecQuery(query);

				query = new Query(conn,
					@"DELETE FROM Images 
					WHERE ID={imageid} AND CollectionID={collectionid}");
				query.AddParam("imageid", image.ID.ID);
				query.AddParam("collectionid", id);
				conn.ExecQuery(query);

				// only remove physical images if no other record points to same resource
				query = new Query(conn,
					@"SELECT COUNT(*) FROM Images WHERE Resource={resource}");
				query.AddParam("resource", image.Resource);
				removeresource = (conn.DataToInt(conn.ExecScalar(query), 0) == 0);
			}
			if (removeresource)
				DeleteImageResource(image.Resource);
		}

		/// <summary>
		/// Delete physical image files
		/// </summary>
		/// <remarks>
		/// Each image record can have several different physical image files associated with it.
		/// This method tries to remove the different files.
		/// </remarks>
		/// <param name="resource">The resource of the image record</param>
		/// <returns><c>true</c> if all files were successfully removed (or did not exist anyway), 
		/// <c>false</c> if one or more files could not be removed.</returns>
		internal virtual bool DeleteImageResource(string resource)
		{
			if (IsExtendedResource(resource))
				return false;
			bool result = true;
			try
			{
				string path = MapResource(resource, ImageSize.Full);
				if (File.Exists(path))
					File.Delete(path);
			}
			catch
			{
				result = false;
			}
			try
			{
				string path = MapResource(resource, ImageSize.Medium);
				if (File.Exists(path))
					File.Delete(path);
			}
			catch
			{
				result = false;
			}
			try
			{
				string path = MapResource(resource, ImageSize.Thumbnail);
				if (File.Exists(path))
					File.Delete(path);
			}
			catch
			{
				result = false;
			}
			return result;
		}

		internal static string CreateImageResource(int cid, int id)
		{
			string prefix = (Configuration.Instance.ContainsKey("content.resourceprefix") ?
				Configuration.Instance.GetString("content.resourceprefix") : "");
			return String.Format("{0}_{1}_{2}.jpg", prefix, cid, id);
		}

		/// <summary>
		/// Create an image
		/// </summary>
		/// <param name="personal">Specifies if the newly created image should
		/// be a personal image</param>
		/// <remarks>
		/// This method creates a new image object in this collection.  To save the image,
		/// call <see cref="ModifiableObject.Update()"/>.
		/// </remarks>
		/// <returns>A newly created image object assigned to this collection.</returns>
		public virtual Image CreateImage(bool personal)
		{
			User current = User.CurrentUser();
			if (!CanCreateImage(current, personal))
				throw new PrivilegeException("Insufficient privileges to create image");
			Image image = new Image(new ImageIdentifier(0, id), null);
			if (personal)
			{
				image.userid = current.ID;
				image.username = current.FullName;
			}
			image.MarkModified();
			image.createddate = DateTime.Now;
			return image;
		}

		/// <summary>
		/// Store a newly created or modified image object in the database
		/// </summary>
		/// <remarks>
		/// This method stores an image in the database.  It should not be called directly, use
		/// <see cref="ModifiableObject.Update()"/> instead.
		/// </remarks>
		/// <param name="image">The image to store in the database.</param>
		internal virtual void UpdateImage(Image image)
		{
			using (DBConnection conn = DBConnector.GetConnection())
			{
				int result;
				Query query;
				if (image.ID.ID == 0) 
				{
					query = new Query(conn,
						@"INSERT INTO Images (CollectionID,UserID,
						Created,Modified,CachedUntil,Expires,
						RemoteID,RecordStatus,Resource) 
						VALUES ({collectionid},{userid},
						{created:n},{modified:n},{cacheduntil:n},{expires:n},
						{remoteid},{recordstatus},{resource})");
					query.AddParam("collectionid", id);
					query.AddParam("userid", image.userid);
					query.AddParam("created", image.createddate);
					query.AddParam("modified", image.modifieddate);
					query.AddParam("cacheduntil", image.cacheduntil);
					query.AddParam("expires", image.expires);
					query.AddParam("remoteid", image.remoteid);
					query.AddParam("recordstatus", image.recordstatus);
					query.AddParam("resource", (image.Resource == null ? "" : image.Resource));
					result = conn.ExecQuery(query);

					if (result == 1)
					{
						image.SetID(new ImageIdentifier(conn.LastIdentity("Images"), id));
						if (image.Resource == null || image.Resource.Length == 0)
							image.SetResource(CreateImageResource(id, image.ID.ID));
						query = new Query(conn,
							@"UPDATE Images SET Resource={resource} 
							WHERE ID={id} AND CollectionID={collectionid}");
						query.AddParam("resource", image.Resource);
						query.AddParam("id", image.ID.ID);
						query.AddParam("collectionid", image.ID.CollectionID);
						conn.ExecQuery(query);
					}
					else
						throw new CoreException("Could not write new Image object to database");
				}
				else
				{
					if (image.Resource == null || image.Resource.Length == 0)
						image.SetResource(CreateImageResource(id, image.ID.ID));
					query = new Query(conn,
						@"UPDATE Images SET Resource={resource},
						Modified={modified:n},CachedUntil={cacheduntil:n},Expires={expires:n},
						RemoteID={remoteid},RecordStatus={recordstatus} 
						WHERE ID={id} AND CollectionID={collectionid}");
					query.AddParam("resource", image.Resource);
					query.AddParam("id", image.ID.ID);
					query.AddParam("collectionid", id);
					query.AddParam("modified", image.modifieddate);
					query.AddParam("cacheduntil", image.cacheduntil);
					query.AddParam("expires", image.expires);
					query.AddParam("remoteid", image.remoteid);
					query.AddParam("recordstatus", image.recordstatus);
					result = conn.ExecQuery(query);
					if (result != 1)
						throw new CoreException("Could not write modified Image object to database");
				}
				foreach (FieldData data in image.fielddata.Values)
					data.WriteToDB(conn);
			}		
		}

		internal void DeleteUserImages(User user)
		{
			if (user == null || user.ID == 0)
				return;
			ArrayList resources = new ArrayList();
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT Resource FROM Images WHERE UserID={userid} AND CollectionID={collectionid}");
				query.AddParam("userid", user.ID);
				query.AddParam("collectionid", id);
				using (IDataReader reader = conn.ExecReader(query))
					while (reader.Read())
						resources.Add(conn.DataToString(reader.GetValue(0)));

				if (resources.Count == 0)
					return;

				query = new Query(conn,
					@"DELETE FROM FieldData WHERE CollectionID={collectionid} " +
					"AND ImageID IN (SELECT ID FROM Images WHERE UserID={userid} AND CollectionID={collectionid})");
				query.AddParam("userid", user.ID);
				query.AddParam("collectionid", id);
				conn.ExecQuery(query);

				query = new Query(conn,
					@"DELETE FROM Images WHERE UserID={userid} AND CollectionID={collectionid}");
				query.AddParam("userid", user.ID);
				query.AddParam("collectionid", id);
				conn.ExecQuery(query);
			}
			foreach (string r in resources)
				DeleteImageResource(r);
		}

		/// <summary>
		/// returns the internal identifier for this collection
		/// </summary>
		/// <remarks>
		/// This property is read-only. The internal identifier is the primary key in the database
		/// for this collection.
		/// </remarks>
		/// <value>The internal identifier for this collection</value>
		public int ID
		{
			get
			{
				return id;
			}
		}

		/// <summary>
		/// Internal identifier
		/// </summary>
		/// <remarks>
		/// The internal identifier is the primary key used in the database.
		/// </remarks>
		/// <returns>
		/// The internal identifier of this collection.
		/// </returns>
		public override int GetID()
		{
			return id;
		}

		/// <summary>
		/// Return the class identifier
		/// </summary>
		/// <remarks>
		/// Every class derived from <see cref="CachableObject"/> must implement this method. 
		/// It must return a string that is unique in the system. 
		/// The suggested format is <c>Namespace.Class</c>.
		/// </remarks>
		/// <returns>A unique string identifier for this class</returns>
		protected override string GetClassIdentifier()
		{
			return classid;
		}

		/// <summary>
		/// Adds a <see cref="Field"/> to this collection
		/// </summary>
		/// <remarks>The <see cref="Field"/> will be written to the database at this
		/// point. The field must be newly created. It is not necessary to call 
		/// <see cref="ModifiableObject.Update"/> afterwards in order to commit the field to the database.
		/// <seealso cref="AddField(Field)"/>
		/// </remarks>
		/// <param name="f">The field to add</param>
		/// <param name="before">The field in the collection before which the new field should be added.
		/// If this is null, the new field is added at the end.</param>
		/// <exception cref="CoreException">Thrown if the field specified by the before parameter is not in this collection.</exception>
		public void AddField(Field f, Field before)
		{
			User.RequirePrivilege(Privilege.ManageCollection, this);
			f.SetCollectionID(id);
			f.Update();
			using (DBConnection conn = DBConnector.GetConnection())
			{
				int displayorder;				
				Query query;
				if (before == null)
				{
					query = new Query(conn,
						@"SELECT MAX(DisplayOrder) FROM FieldDefinitions 
						WHERE CollectionID={collectionid}");
					query.AddParam("collectionid", id);
					displayorder = conn.DataToInt(conn.ExecScalar(query), 0) + 1;					
					fields.Add(f);
				}
				else
				{
					if (!fields.Contains(before))
						throw new CoreException("Specified field to insert before is not in this collection.");

					query = new Query(conn,
						@"SELECT DisplayOrder FROM FieldDefinitions WHERE ID={id}");
					query.AddParam("id", before.ID);
					displayorder = conn.DataToInt(conn.ExecScalar(query), 0);

					query = new Query(conn,
						@"UPDATE FieldDefinitions SET DisplayOrder=DisplayOrder+1 
						WHERE CollectionID={collectionid} AND DisplayOrder>={displayorder}");
					query.AddParam("collectionid", id);
					query.AddParam("displayorder", displayorder);
					conn.ExecQuery(query);

					fields.Insert(fields.IndexOf(before), f);
				}
				query = new Query(conn,
					@"UPDATE FieldDefinitions SET DisplayOrder={displayorder} WHERE ID={id}");
				query.AddParam("displayorder", displayorder);
				query.AddParam("id", f.ID);
				conn.ExecQuery(query);
			}
		}

		/// <summary>
		/// Adds a <see cref="Field"/> to this collection
		/// </summary>
		/// <remarks>The <see cref="Field"/> will be written to the database at this
		/// point. The field must be newly created. It is not necessary to call 
		/// <see cref="ModifiableObject.Update"/> afterwards in order to commit the field to the database.
		/// <seealso cref="AddField(Field,Field)"/>
		/// </remarks>
		/// <example>
		/// For an example, see <see cref="AddField(Field,Field)"/>.
		/// </example>
		/// <param name="f">The field to add. It will be placed after all other existing fields.</param>
		public void AddField(Field f)
		{
			AddField(f, null);
		}

		/// <summary>
		/// Removes a <see cref="Field"/> from this collection
		/// </summary>
		/// <remarks>
		/// Calling this method will delete all the data stored in this database for this field.
		/// </remarks>
		/// <param name="f">The field to be removed. This change is committed to the database 
		/// immediately.</param>
		/// <exception cref="CoreException">Thrown if a specified field is not in this 
		/// collection</exception>
		public void RemoveField(Field f)
		{
			User.RequirePrivilege(Privilege.ManageCollection, this);
			if (!fields.Contains(f))
				throw new CoreException("Specified field is not in this collection.");
			fields.Remove(f);
			f.Delete();
		}

		/// <summary>
		/// Changes the order of <see cref="Field"/>s within this collection
		/// </summary>
		/// <remarks>
		/// Changing the order of fields within a collection has no effect on the data
		/// stored for every <see cref="Image"/>, it only affects the presentation of the
		/// data to the user.</remarks>
		/// <param name="f">The field to be moved to a different position</param>
		/// <param name="before">The field before which the moving field is inserted, or <c>null</c>
		/// if the moving field should be placed as the last field of the collection.</param>
		/// <exception cref="System.Exception">Thrown if a specified field is not in this 
		/// collection</exception>
		public void MoveField(Field f, Field before)
		{
			User.RequirePrivilege(Privilege.ManageCollection, this);
			if (!fields.Contains(f) || (before != null && !fields.Contains(before)))
				throw new CoreException("Specified field is not in this collection.");
			using (DBConnection conn = DBConnector.GetConnection())
			{
				int displayorder;
				Query query;
				fields.Remove(f);			
				if (before == null)
				{
					query = new Query(conn,
						@"SELECT MAX(DisplayOrder) FROM FieldDefinitions 
						WHERE CollectionID={collectionid}");
					query.AddParam("collectionid", id);
					displayorder = conn.DataToInt(conn.ExecScalar(query), 0) + 1;
					fields.Add(f);
				}
				else
				{
					query = new Query(conn,
						@"SELECT DisplayOrder FROM FieldDefinitions WHERE ID={id}");
					query.AddParam("id", before.ID);
					displayorder = conn.DataToInt(conn.ExecScalar(query), 0);

					query = new Query(conn,
						@"UPDATE FieldDefinitions SET DisplayOrder=DisplayOrder+1 
						WHERE CollectionID={collectionid} AND DisplayOrder>={displayorder}");
					query.AddParam("collectionid", id);
					query.AddParam("displayorder", displayorder);
					conn.ExecQuery(query);

					fields.Insert(fields.IndexOf(before), f);
				}
				query = new Query(conn,
					@"UPDATE FieldDefinitions SET DisplayOrder={displayorder} WHERE ID={id}");
				query.AddParam("displayorder", displayorder);
				query.AddParam("id", f.ID);
				conn.ExecQuery(query);
			}
		}

		/// <summary>
		/// Identifying character
		/// </summary>
		/// <remarks>
		/// The identifying character for collections within the access control system.
		/// Every class using access control must have a different character.
		/// </remarks>
		/// <returns>
		/// The identifying character <c>'C'</c>.
		/// </returns>
		public char GetObjectTypeIdentifier()
		{
			return 'C';
		}

		/// <summary>
		/// Collection Type
		/// </summary>
		/// <remarks>
		/// This property must be overridden by classes changing the behaviour of a regular
		/// collection. This property is read-only.
		/// </remarks>
		/// <value>
		/// Always returns <see cref="CollectionType.Internal"/>.
		/// </value>
		public virtual CollectionType Type
		{
			get
			{
				return CollectionType.Internal;
			}
		}

		/// <summary>
		/// Cache restriction
		/// </summary>
		/// <remarks>
		/// Specifies what image sizes may be cached on a client
		/// </remarks>
		/// <value>
		/// The cache restriction for this collection.
		/// </value>
		public virtual ImageCaching Caching
		{
			get
			{
				return caching;
			}
			set
			{
				MarkModified();
				caching = value;
			}
		}

		/// <summary>
		/// Group membership
		/// </summary>
		/// <remarks>
		/// Collections can be organized in groups.  See <see cref="CollectionGroup"/>.
		/// </remarks>
		/// <value>
		/// The internal identifier of the collection group this collection belongs to,
		/// or <c>0</c> if this collection does not belong to a collection group.
		/// </value>
		public int Group
		{
			get
			{
				return group;
			}
			set
			{
				MarkModified();
				group = value;
			}
		}

		/// <summary>
		/// Access control object
		/// </summary>
		/// <remarks>
		/// This method returns the associated <see cref="AccessControl"/> object.
		/// </remarks>
		/// <returns>The access control object for this collection.</returns>
		public virtual IAccessControl GetAccessControl()
		{
			return accesscontrol;
		}

		/// <summary>
		/// The owner of the collection
		/// </summary>
		/// <remarks>
		/// Since collections do not have owners, this method always returns <c>0</c>.
		/// </remarks>
		/// <returns><c>0</c></returns>
		public int GetOwner()
		{
			return 0;
		}

		/// <summary>
		/// Return privileges relevant for this class
		/// </summary>
		/// <remarks>
		/// Not all available <see cref="Privilege"/>s are relevant for every
		/// access controlled class. This method must return the privileges that
		/// are relevant for this class.
		/// </remarks>
		/// <returns>A combined <see cref="Privilege"/> of all relevant privileges for
		/// this class</returns>
		public Privilege GetRelevantPrivileges()
		{
			return
				Privilege.ModifyACL |
				Privilege.ManageCollection |
				Privilege.DeleteCollection |
				Privilege.ModifyImages |
				Privilege.ReadCollection |
				Privilege.FullSizedImages |
				Privilege.AnnotateImages |
				Privilege.ManageControlledLists |
				Privilege.PersonalImages |
				Privilege.ShareImages |
				Privilege.SuggestImages;
		}

		/// <summary>
		/// Checks for keyword searchable fields
		/// </summary>
		/// <remarks>
		/// A keyword search on a collection is only possible if at least one
		/// field is keyword searchable.
		/// </remarks>
		/// <value>
		/// <c>true</c> if there is at least one keyword searchable field in this 
		/// collection, <c>false</c> otherwise.
		/// </value>
		public virtual bool IsKeywordSearchable
		{
			get
			{
				foreach (Field f in GetFields())
					if (f.KeywordSearchable)
						return true;
				return false;
			}
		}

		/// <summary>
		/// Checks for browsable fields
		/// </summary>
		/// <remarks>
		/// A collection is considered browsable if any field in the collection is browsable.
		/// </remarks>
		/// <value><c>true</c> if at least one field is browsable, <c>false</c> otherwise.
		/// </value>
		public virtual bool IsBrowsable
		{
			get
			{
				foreach (Field f in GetFields())
					if (f.Browsable)
						return true;
				return false;
			}
		}

		/// <summary>
		/// Indicates if record history is kept
		/// </summary>
		/// <remarks>
		/// If this property is <c>true</c>, any change to an image record will cause the
		/// original values to be written to a record history table in the database.
		/// For the base collection class, this property is always <c>true</c>.
		/// </remarks>
		/// <value><c>true</c></value>
		public virtual bool HasRecordHistory
		{
			get
			{
				return true;
			}
		}

		/// <summary>
		/// Read only flag
		/// </summary>
		/// <remarks>
		/// Some collections are read only.  For the base collection class, this
		/// property is always <c>false</c>.
		/// </remarks>
		/// <value><c>false</c></value>
		public virtual bool IsReadOnly
		{
			get
			{
				return false;
			}
		}

		/// <summary>
		/// Get list of all image resources
		/// </summary>
		/// <remarks>
		/// This method builds a list of all image resources in this collection
		/// </remarks>
		/// <param name="includeextended">Flag determining if extended resources should
		/// be included in the result</param>
		/// <param name="includepersonal">Flag determining if personal images should
		/// be included in the result</param>
		/// <returns>A hashtable with image identifiers as keys and image resources
		/// as values.</returns>
		public Hashtable GetAllImageResources(bool includeextended, bool includepersonal)
		{
			Hashtable result = new Hashtable();
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT ID,Resource FROM Images WHERE CollectionID={collectionid}" +
					(includepersonal ? "" : " AND (UserID IS NULL OR UserID=0)"));
				query.AddParam("collectionid", id);
				DataTable table = conn.SelectQuery(query);
				foreach (DataRow row in table.Rows)
				{
					string resource = conn.DataToString(row["Resource"]);
					if (includeextended || !IsExtendedResource(resource))
						result.Add(new ImageIdentifier(conn.DataToInt(row["ID"], 0), id), resource);
				}
			}
			return result;
		}

		/// <summary>
		/// Get file listing
		/// </summary>
		/// <remarks>
		/// This method returns a list of all available resource files for this collection.
		/// The path to each file is relative to the <see cref="PhysicalResourcePathRoot"/>
		/// for the collection.
		/// </remarks>
		/// <returns>An array of paths to resource files</returns>
		public string[] GetAvailableFiles()
		{
			User.RequirePrivilege(Privilege.ManageCollection, this);
			ArrayList files = new ArrayList();
			string root = PhysicalResourcePathRoot;
			if (root == "")
				return null;
			GetAvailableFilesFromDirectory(ref files, root, "");
			return (files != null ? (string[])files.ToArray(typeof(string)) : null);
		}

		private void GetAvailableFilesFromDirectory(ref ArrayList files, string basedir, string dir)
		{
			string currentdir = Path.Combine(basedir, dir);
			if (Directory.Exists(currentdir))
			{
				foreach (string file in Directory.GetFiles(currentdir))
					files.Add(file.Substring(basedir.Length));
				foreach (string subdir in Directory.GetDirectories(currentdir))
					GetAvailableFilesFromDirectory(ref files, basedir, Path.Combine(dir, subdir));
			}
		}

		/// <summary>
		/// Get last modification date for image resource
		/// </summary>
		/// <remarks>
		/// This function returns the last modification date for a particular image
		/// file, identified by its resource and desired format.  If the resource
		/// cannot be found or is not accessible, <c>Date.MinValue</c> is returned.
		/// </remarks>
		/// <param name="resource">A string identifying the image resource</param>
		/// <param name="format">The desired image format</param>
		/// <returns>The last modification date, or <c>Date.MinValue</c> if 
		/// the file is not accessible or is not found.</returns>
		public DateTime GetResourceModificationDate(string resource, ImageSize format)
		{
			return GetResourceModificationDate(true, resource, format);
		}

		internal virtual DateTime GetResourceModificationDate(bool enforcepriv, string resource, ImageSize format)
		{
			// when requesting full-size image without privilege, fall back to medium
			if (enforcepriv && format == ImageSize.Full && !User.HasPrivilege(Privilege.FullSizedImages, this))
				return GetResourceModificationDate(enforcepriv, resource, ImageSize.Medium);
			if (enforcepriv && !User.HasPrivilege(Privilege.ReadCollection, this))
				return DateTime.MinValue;
			try
			{
				return File.GetLastWriteTime(MapResource(resource, format));
			}
			catch
			{
				return DateTime.MinValue;
			}		
		}

		/// <summary>
		/// Resource size
		/// </summary>
		/// <remarks>
		/// This method returns the size of an image resource - usually the file size of a JPEG
		/// file on the hard drive.
		/// </remarks>
		/// <param name="resource">The resource of the image</param>
		/// <param name="format">The image size for which to return the resource size</param>
		/// <returns>The size of the resource in bytes, or <c>-1</c> if an error occured</returns>
		public long GetResourceSize(string resource, ImageSize format)
		{
			return GetResourceSize(true, resource, format);
		}

		/// <summary>
		/// Resource size
		/// </summary>
		/// <remarks>
		/// This method returns the size of an image resource - usually the file size of a JPEG
		/// file on the hard drive.
		/// </remarks>
		/// <param name="enforcepriv">Privilege enforcement; if <c>true</c> this method
		/// will return <c>-1</c> if the current user does not have read access to the
		/// collection.</param>
		/// <param name="resource">The resource of the image</param>
		/// <param name="format">The image size for which to return the resource size</param>
		/// <returns>The size of the resource in bytes, or <c>-1</c> if an error occured</returns>
		internal virtual long GetResourceSize(bool enforcepriv, string resource, ImageSize format)
		{
			// when requesting full-size image without privilege, fall back to medium
			if (enforcepriv && format == ImageSize.Full && !User.HasPrivilege(Privilege.FullSizedImages, this))
				return GetResourceSize(enforcepriv, resource, ImageSize.Medium);
			if (enforcepriv && !User.HasPrivilege(Privilege.ReadCollection, this))
				return -1;
			try
			{
				return new FileInfo(MapResource(resource, format)).Length;
			}
			catch
			{
				return -1;
			}		
		}

		/// <summary>
		/// returns a stream containing the JPEG image data
		/// </summary>
		/// <remarks>
		/// This method returns the actual image data for a specified resource in the requested size.
		/// There is always a valid JPEG returned. If the resource is not found, access is denied, or
		/// the file cannot be read, the JPEG image will contain an error message.
		/// </remarks>
		/// <param name="resource">The resource for which to return the image data</param>
		/// <param name="format">The size of the requested image</param>
		/// <returns>A stream containing the image data, or <c>null</c> if an exception occurred.
		/// </returns>
		public Stream GetResourceData(string resource, ImageSize format)
		{
			return GetResourceData(true, resource, format);
		}

		internal virtual Stream GetResourceData(bool enforcepriv, string resource, ImageSize format)
		{
			// when requesting full-size image without privilege, fall back to medium
			if (format == ImageSize.Full && !User.HasPrivilege(Privilege.FullSizedImages, this))
				return GetResourceData(enforcepriv, resource, ImageSize.Medium);
			if (enforcepriv && !User.HasPrivilege(Privilege.ReadCollection, this))
				return RenderMessage("Access denied", format);
			bool check = CheckImageRecordForResource(resource);
			if (check)
			{
				string file = MapResource(resource, format);
				if (!File.Exists(file))
				{
					TransactionLog.Instance.Add("GetResource failed", String.Format("File '{0}' not found", file));
					return RenderMessage("File not found", format);
				}
				try
				{
					Stream instream = new FileStream(
						file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
					return instream;
				}
				catch (Exception e)
				{
					TransactionLog.Instance.AddException("GetResource failed", 
						String.Format("File '{0}' not accessible", file), e);
					return RenderMessage("File not accessible", format);
				}
			}
			else
			{
				return RenderMessage("Image not available [1]", format);
			}
		}

		/// <summary>
		/// returns a stream containing the JPEG image data
		/// </summary>
		/// <remarks>
		/// This method returns the actual image data for a specified resource formatted according
		/// to the specified parameters.
		/// There is always a valid JPEG returned. If the resource is not found, access is denied, or
		/// the file cannot be read, the JPEG image will contain an error message.
		/// </remarks>
		/// <param name="resource">The resource for which to return the image data</param>
		/// <param name="parameters">The formatting parameters for the requested image</param>
		/// <returns>A stream containing the image data, or <c>null</c> if an exception occurred.
		/// </returns>
		public virtual Stream GetResourceData(string resource, Parameters parameters)
		{
			ImageSize size = User.HasPrivilege(Privilege.FullSizedImages, this) ?
				ImageSize.Full :
				ImageSize.Medium;
			ImageSettings settings = GetImageSettings(size);
			int width = Math.Min(parameters.TargetWidth, settings.width);
			int height = Math.Min(parameters.TargetHeight, settings.height);
			Parameters p = new Parameters(width, height, parameters.ExactTargetSize, parameters.BorderColor,
				parameters.CropX, parameters.CropY, parameters.CropW, parameters.CropH,
				parameters.Quality);
			string root = resourcepath.PhysicalRoot;
			string sourcefile = MapResource(resource, size).Substring(root.Length);
			MediaRepository rep = new MediaRepository(root);
			Stream result = null;
			try
			{
				result = rep.RetrieveFile(Path.GetFileName(sourcefile), Path.GetDirectoryName(sourcefile),
					"image/jpeg", parameters);
			}
			catch (Exception ex)
			{
				TransactionLog.Instance.AddException("GetResource failed", 
					String.Format("Resource: {0}\nParameters {1}", resource, parameters), ex);
				result = null;
			}
			if (result == null)
				return RenderMessage("Image not available [3]", 
					parameters.TargetWidth > 0 ? parameters.TargetWidth : 320,
					parameters.TargetHeight > 0 ? parameters.TargetHeight : 240);
			else
				return result;
		}

		/// <summary>
		/// Checks if an image record exists for a given resource
		/// </summary>
		/// <remarks>
		/// This is a utility method used by <see cref="GetResourceData(string, ImageSize)"/>.  It prevents
		/// users from requesting images (or other files) that are not a valid resource for
		/// one of the images in a collection.
		/// </remarks>
		/// <param name="resource">The image resource to check</param>
		/// <returns><c>true</c> if at least one image record with the given resource exists,
		/// <c>false</c> otherwise</returns>
		protected virtual bool CheckImageRecordForResource(string resource)
		{
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT COUNT(*) AS C FROM Images 
					WHERE CollectionID={collectionid} AND Resource={resource}");
				query.AddParam("collectionid", this.id);
				query.AddParam("resource", resource);
				return (conn.DataToInt(conn.ExecScalar(query), 0) > 0);
			} 
		}

		private void SetImageFlags(ImageFlag flags, bool add, User user, params ImageIdentifier[] ids)
		{
			if (ids == null || ids.Length == 0)
				return;
			ArrayList incollection = new ArrayList();
			foreach (ImageIdentifier id in ids)
				if (id.CollectionID == this.id)
					incollection.Add(id.ID);
			if (incollection.Count == 0)
				return;
			if (user != null)
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"UPDATE Images SET Flags=COALESCE(Flags,0)" + 
						(add ? "|" : "&~") +
						"{flags} WHERE ID IN {ids} AND CollectionID={collectionid} AND UserID={userid}");
					query.AddParam("flags", flags);
					query.AddParam("collectionid", this.id);
					query.AddParam("userid", user.ID);
					query.AddParam("ids", incollection);
					conn.ExecQuery(query);
				}
		}

		/// <summary>
		/// Share images
		/// </summary>
		/// <remarks>
		/// Personal images can be flagged as shared.  Shared images
		/// can be used by other users who themselves have access to the collection.
		/// </remarks>
		/// <param name="share"><c>true</c> if the specified images should be shared,
		/// <c>false</c> otherwise</param>
		/// <param name="ids">An array of internal identifiers of images for which to allow or
		/// disallow sharing</param>
		public void ShareImage(bool share, params ImageIdentifier[] ids)
		{
			User.RequirePrivilege(Privilege.ShareImages, this);
			SetImageFlags(ImageFlag.Shared, share, User.CurrentUser(), ids);
		}

		/// <summary>
		/// Suggest images for inclusion
		/// </summary>
		/// <remarks>
		/// Personal images can be flagged as suggested for inclusion.  Images suggested for inclusion
		/// can be reviewed by a collection administrator and be rejected or included as regular
		/// images in the collection.
		/// </remarks>
		/// <param name="suggest"><c>true</c> if the specified images should be suggested,
		/// <c>false</c> otherwise</param>
		/// <param name="ids">An array of internal identifiers of images to suggest or
		/// no longer suggest for inclusion</param>
		public void SuggestImageForInclusion(bool suggest, params ImageIdentifier[] ids)
		{
			User.RequirePrivilege(Privilege.SuggestImages, this);
			SetImageFlags(ImageFlag.InclusionSuggested, suggest, User.CurrentUser(), ids);
		}

		/// <summary>
		/// Reject suggested image
		/// </summary>
		/// <remarks>
		/// Personal images can be flagged as suggested for inclusion.  Images suggested for inclusion
		/// can be reviewed by a collection administrator and be rejected or included as regular
		/// images in the collection.  This method rejects a given image.
		/// </remarks>
		/// <param name="owner">The owner of the image to reject</param>
		/// <param name="id">The image identifier of the image to reject</param>
		public void RejectImageForInclusion(User owner, ImageIdentifier id)
		{
			User.RequirePrivilege(Privilege.ModifyImages, this);
			SetImageFlags(ImageFlag.InclusionRefused, true, owner, id);
			SetImageFlags(ImageFlag.InclusionSuggested, false, owner, id);
		}

		/// <summary>
		/// Reject suggested image
		/// </summary>
		/// <remarks>
		/// Personal images can be flagged as suggested for inclusion.  Images suggested for inclusion
		/// can be reviewed by a collection administrator and be rejected or included as regular
		/// images in the collection.  This method accepts a given image.
		/// </remarks>
		/// <param name="owner">The owner of the image to accept</param>
		/// <param name="id">The image identifier of the image to accept</param>
		public void AcceptImageForInclusion(User owner, ImageIdentifier id)
		{
			if (owner == null || id.CollectionID != this.id)
				return;
			User.RequirePrivilege(Privilege.ModifyImages, this);
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"UPDATE Images SET Flags=0,UserID=0 
					WHERE ID={id} AND CollectionID={collectionid} AND UserID={userid}");
				query.AddParam("id", id.ID);
				query.AddParam("collectionid", id.CollectionID);
				query.AddParam("userid", owner.ID);
				conn.ExecQuery(query);
			}
		}

		/// <summary>
		/// List of suggested images
		/// </summary>
		/// <remarks>
		/// This method creates a list of image identifiers of all images suggested for
		/// inclusion in this collection.
		/// </remarks>
		/// <returns>An array of image identifiers of suggested images</returns>
		public ImageIdentifier[] GetSuggestedImages()
		{
			User.RequirePrivilege(Privilege.ModifyImages, this);
			int[] r = null;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT ID FROM Images WHERE CollectionID={collectionid} 
					AND (UserID!=0) AND (Flags&{flags}={flags})");
				query.AddParam("collectionid", this.id);
				query.AddParam("flags", ImageFlag.InclusionSuggested);
				r = conn.TableToArray(conn.SelectQuery(query));
			}
			if (r == null)
				return null;
			ImageIdentifier[] result = new ImageIdentifier[r.Length];
			for (int i = 0; i < r.Length; i++)
				result[i] = new ImageIdentifier(r[i], id);
			return result;
		}

		/// <summary>
		/// Move image between collections
		/// </summary>
		/// <remarks>
		/// This method takes two image objects as parameters.  The first is the source image,
		/// which must be from the collection this method is called on.  The second is the
		/// target image, which must have been created by calling Collection.CreateImage on the
		/// target collection.  The Update method of the target image must not have been
		/// called yet, it will be called by this method.  The source image will be deleted 
		/// after the all information is transferred to the target image.  All references to
		/// the source image in slideshows, annotations, etc. will be updated to point to
		/// the target image instead.
		/// </remarks>
		/// <param name="source">The source image</param>
		/// <param name="target">The target image</param>
		public bool MoveImageTo(Image source, Image target)
		{
			User.RequirePrivilege(Privilege.ModifyImages, this);
			Collection targetcoll = Collection.GetByID(target.ID.CollectionID);
			User.RequirePrivilege(Privilege.ModifyImages, targetcoll);
			if (source.ID.CollectionID != this.id)
				throw new CoreException("MoveImageTo() must be called on collection that " +
					"contains source image");
			if (target.ID.CollectionID == this.id)
				throw new CoreException("Cannot move image to source collection");
			if (target.ID.ID != 0)
				throw new CoreException("Update() has already been called on target image");

			target.createddate = source.createddate;
			target.Update();

			Stream stream;
			bool success = true;
			stream = this.GetResourceData(false, source.Resource, ImageSize.Full);
			success = success && targetcoll.SetRawResourceData(target.Resource, ImageSize.Full, stream);
			stream.Close();
			stream = this.GetResourceData(false, source.Resource, ImageSize.Medium);
			success = success && targetcoll.SetRawResourceData(target.Resource, ImageSize.Medium, stream);
			stream.Close();
			stream = this.GetResourceData(false, source.Resource, ImageSize.Thumbnail);
			success = success && targetcoll.SetRawResourceData(target.Resource, ImageSize.Thumbnail, stream);
			stream.Close();

			if (!success)
			{
				target.Delete();
				return false;
			}

			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"UPDATE FavoriteImages SET ImageID={targetid},CollectionID={targetcid} 
					WHERE ImageID={sourceid} AND CollectionID={sourcecid}");
				query.AddParam("targetid", target.ID.ID);
				query.AddParam("targetcid", target.ID.CollectionID);
				query.AddParam("sourceid", source.ID.ID);
				query.AddParam("sourcecid", source.ID.CollectionID);
				conn.ExecQuery(query);

				query = new Query(conn,
					@"UPDATE ImageAnnotations SET ImageID={targetid},CollectionID={targetcid} 
					WHERE ImageID={sourceid} AND CollectionID={sourcecid}");
				query.AddParam("targetid", target.ID.ID);
				query.AddParam("targetcid", target.ID.CollectionID);
				query.AddParam("sourceid", source.ID.ID);
				query.AddParam("sourcecid", source.ID.CollectionID);
				conn.ExecQuery(query);

				query = new Query(conn,
					@"UPDATE Slides SET ImageID={targetid},CollectionID={targetcid} 
					WHERE ImageID={sourceid} AND CollectionID={sourcecid}");
				query.AddParam("targetid", target.ID.ID);
				query.AddParam("targetcid", target.ID.CollectionID);
				query.AddParam("sourceid", source.ID.ID);
				query.AddParam("sourcecid", source.ID.CollectionID);
				conn.ExecQuery(query);

				query = new Query(conn,
					@"UPDATE RecordMaintenance SET ImageID={targetid},CollectionID={targetcid} 
					WHERE ImageID={sourceid} AND CollectionID={sourcecid}");
				query.AddParam("targetid", target.ID.ID);
				query.AddParam("targetcid", target.ID.CollectionID);
				query.AddParam("sourceid", source.ID.ID);
				query.AddParam("sourcecid", source.ID.CollectionID);
				conn.ExecQuery(query);
			}

			source.Delete();
			return true;
		}

		internal virtual bool SetResourceData(string resource, ImageSize format, Stream stream, string mimetype)
		{
			string tempfile = Path.GetTempFileName();
			string sourcefile = tempfile + "." + MimeType.GetExtension(mimetype);
			File.Move(tempfile, sourcefile);
			using (Stream tempstream = File.OpenWrite(sourcefile))
				Tools.CopyStream(stream, tempstream);
			try
			{
				return SetResourceData(resource, format, sourcefile);
			}
			finally
			{
				File.Delete(sourcefile);
			}
		}

		/// <summary>
		/// Store image data for a given resource
		/// </summary>
		/// <remarks>
		/// This method reads an image from the given stream, resizes, converts and compresses it
		/// into a JPEG file according to the collection settings and stores the file under
		/// the specified resource.
		/// </remarks>
		/// <returns>
		/// <c>true</c> if the resource was stored correctly, <c>false</c> if an exception 
		/// occurred, for example because of an unknown image format in the stream.
		/// </returns>
		/// <param name="resource">The resource for which to store the image data</param>
		/// <param name="format">The format of the image data</param>
		/// <param name="sourcefile">The source image file</param>
		internal virtual bool SetResourceData(string resource, ImageSize format, string sourcefile)
		{
			ImageSettings target = this.GetImageSettings(format);
			Parameters parameters = new Parameters(target.width, target.height,
				target.fixedsize, 
				System.Drawing.Color.FromArgb(255, System.Drawing.Color.FromArgb(target.backgroundcolor)), 
				target.quality);
			try
			{
				return MediaConverter.Convert(sourcefile, MapResource(resource, format), 
					parameters);
			}
			catch (Exception e)
			{
				TransactionLog.Instance.Add("SetResourceData Exception", 
					String.Format("Resource: {0}\n" +
					"MapResource: {1}\n" +
					"Original Exception:\n\n{2}",
					resource,
					MapResource(resource, format),
					e.ToString()));
				return false;
			}
		}
/*
		/// <summary>
		/// Store image data for a given resource
		/// </summary>
		/// <remarks>
		/// This method takes a given image, resizes, converts and compresses it
		/// into a JPEG file according to the collection settings and stores the file under
		/// the specified resource.
		/// </remarks>
		/// <returns>
		/// <c>true</c> if the resource was stored correctly, <c>false</c> if an exception 
		/// occurred, for example because of an unknown image format in the stream.
		/// </returns>
		/// <param name="resource">The resource for which to store the image data</param>
		/// <param name="format">The format of the image data</param>
		/// <param name="image">The image data</param>
		internal virtual bool SetResourceData(string resource, ImageSize format, Bitmap image)
		{
			ImageSettings target = this.GetImageSettings(format);
			try
			{
				if (IsExtendedResource(resource))
					throw new CoreException("Cannot set resource data for extended resources");				
				int width = image.Width;
				int height = image.Height;
				int newwidth = width;
				int newheight = height;
				if (width > target.width || height > target.height)
				{
					float xscale = (width > target.width ? (float)target.width / width : 1);
					float yscale = (height > target.height ? (float)target.height / height : 1);
					newwidth = (xscale < yscale ? target.width : (int)(width * yscale));
					newheight = (xscale < yscale ? (int)(height * xscale) : target.height);
				}
				Bitmap newimage = new Bitmap(target.fixedsize ? target.width : newwidth, 
					target.fixedsize ? target.height : newheight, PixelFormat.Format24bppRgb);
				Graphics g = Graphics.FromImage(newimage);
				if (target.fixedsize)
				{
					Brush brush = new SolidBrush(System.Drawing.Color.FromArgb(255, System.Drawing.Color.FromArgb(target.backgroundcolor)));
					g.FillRectangle(brush, 0, 0, target.width, target.height);
					g.DrawImage(image, (target.width - newwidth) / 2, (target.height - newheight) / 2,
						newwidth, newheight);
				}
				else
				{
					g.DrawImage(image, 0, 0, newwidth, newheight);
				}
				EncoderParameters ep = new EncoderParameters();
				ep.Param[0] = new EncoderParameter(Encoder.Quality, new long[] { target.quality });
				ImageCodecInfo jpegencoder = null;
				foreach (ImageCodecInfo ici in ImageCodecInfo.GetImageEncoders())
					if (ici.FormatDescription == "JPEG")
					{
						jpegencoder = ici;
						break;
					}
				if (jpegencoder == null)
					throw new Exception("No JPEG encoder found");
				FileStream outstream = new FileStream(MapResource(resource, format), FileMode.Create);
				newimage.Save(outstream, jpegencoder, ep);
				outstream.Close();
				return true;
			}
			catch (Exception e)
			{
				TransactionLog.Instance.Add("SetResourceData Exception", 
					String.Format("Resource: {0}\n" +
					"MapResource: {1}\n" +
					"Original Exception:\n\n{2}",
					resource,
					MapResource(resource, format),
					e.ToString()));
				return false;
			}
		}
*/
		/// <summary>
		/// Store image data for a given resource
		/// </summary>
		/// <remarks>
		/// This method stores the given data in a file under
		/// the specified resource.  No content transformation is performed.
		/// </remarks>
		/// <returns>
		/// <c>true</c> if the resource was stored correctly, <c>false</c> if an exception 
		/// occurred.
		/// </returns>
		/// <param name="resource">The resource for which to store the image data</param>
		/// <param name="format">The format of the image data</param>
		/// <param name="data">The byte array containing the image data</param>
		public virtual bool SetRawResourceData(string resource, ImageSize format, byte[] data)
		{
			return SetRawResourceData(true, resource, format, data);
		}

		internal virtual bool SetRawResourceData(bool enforcepriv, string resource, ImageSize format, byte[] data)
		{
			if (enforcepriv)
				User.RequirePrivilege(Privilege.ModifyImages, this);
			try
			{
				FileStream outstream = new FileStream(MapResource(resource, format), FileMode.Create);
				outstream.Write(data, 0, data.Length);
				outstream.Close();
				return true;
			}
			catch (Exception e)
			{
				TransactionLog.Instance.Add("SetRawResourceData Exception", 
					String.Format("Resource: {0}\n" +
					"MapResource: {1}\n" +
					"Original Exception:\n\n{2}",
					resource,
					MapResource(resource, format),
					e.ToString()));
				return false;
			}
		}

		/// <summary>
		/// Store image data for a given resource
		/// </summary>
		/// <remarks>
		/// This method stores the given data in a file under
		/// the specified resource.  No content transformation is performed.
		/// </remarks>
		/// <returns>
		/// <c>true</c> if the resource was stored correctly, <c>false</c> if an exception 
		/// occurred.
		/// </returns>
		/// <param name="resource">The resource for which to store the image data</param>
		/// <param name="format">The format of the image data</param>
		/// <param name="data">The stream containing the image data</param>
		public virtual bool SetRawResourceData(string resource, ImageSize format, Stream data)
		{
			return SetRawResourceData(true, resource, format, data);
		}

		internal virtual bool SetRawResourceData(bool enforcepriv, string resource, ImageSize format, Stream data)
		{
			if (enforcepriv)
				User.RequirePrivilege(Privilege.ModifyImages, this);
			try
			{
				FileStream outstream = new FileStream(MapResource(resource, format), FileMode.Create);
				byte[] buffer = new byte[16384];
				int read;
				do
				{
					read = data.Read(buffer, 0, buffer.Length);
					if (read > 0)
						outstream.Write(buffer, 0, read);
				} while (read > 0);
				outstream.Close();
				return true;
			}
			catch (Exception e)
			{
				TransactionLog.Instance.Add("SetRawResourceData Exception", 
					String.Format("Resource: {0}\n" +
					"MapResource: {1}\n" +
					"Original Exception:\n\n{2}",
					resource,
					MapResource(resource, format),
					e.ToString()));
				return false;
			}
		}

		/// <summary>
		/// Map a resource string to a file name
		/// </summary>
		/// <remarks>
		/// This method maps a resource string to a file name, with consideration to
		/// the requested image size.
		/// </remarks>
		/// <param name="resource">The resource string to map</param>
		/// <param name="format">The format/size of the image</param>
		/// <returns>A file path to the image file</returns>
		public virtual string MapResource(string resource, ImageSize format)
		{
			string imageres = resource;
			if (IsExtendedResource(resource))
			{
				ExtendedResource extres = GetExtendedResource(resource);
				if (extres.IsValid)
					imageres = extres.GetImageResource(format);
				else
					return "";
			}
			return new ResourcePathParser().Parse(resourcepath, imageres, format, true);
		}

		internal ExtendedResource GetExtendedResource(string resource)
		{
			if (IsExtendedResource(resource))
				return new ExtendedResource(
					new ResourcePathParser().Parse(resourcepath, resource, ImageSize.Full, false));
			else
				return null;
		}

		internal static bool IsExtendedResource(string resource)
		{
			if (resource.IndexOfAny(Path.InvalidPathChars) >= 0)
				return false;
			string extension = Path.GetExtension(resource);
			return (extension != null && extension.ToLower() == ".xml");
		}

		/// <summary>
		/// Render a text message in an image
		/// </summary>
		/// <remarks>
		/// This method renders a text message in an image and returns a stream containing
		/// the image in JPEG format.
		/// </remarks>
		/// <param name="message">The message to render</param>
		/// <param name="width">The requested width of the resulting image</param>
		/// <param name="height">The requested height of the resulting image</param>
		/// <returns>A stream containing the image or <c>null</c> of no matching JPEG encoder
		/// was found or an error occurred.</returns>
		public static Stream RenderMessage(string message, int width, int height)
		{
			Bitmap image = new Bitmap(width, height);
			Graphics g = Graphics.FromImage(image);
			g.FillRectangle(new SolidBrush(Color.Black), 0, 0, width, height);
			try
			{
				Bitmap background = new Bitmap(Path.Combine(Configuration.Instance.GetString("content.resourcepath"), "msgback.jpg"));
				if (background != null)
				{
					int x = (width - background.Width) / 2;
					int y = (height - background.Height) / 2;
					g.DrawImage(background, x, y, background.Width, background.Height);
				}
			}
			catch 
			{ 
			}
			g.DrawRectangle(new Pen(Color.LightGray), 0, 0, width - 1, height - 1);
			StringFormat format = new StringFormat();
			format.Alignment = StringAlignment.Center;
			format.LineAlignment = StringAlignment.Center;
			g.DrawString(message, new Font("Arial", 10, FontStyle.Bold), Brushes.LightGray,
				new Rectangle(5, 5, width - 10, height - 10), format);
			MemoryStream stream = new MemoryStream();
			EncoderParameters ep = new EncoderParameters();
			ep.Param[0] = new EncoderParameter(Encoder.Quality, new long[] { 80 });
			ImageCodecInfo jpegencoder = null;
			foreach (ImageCodecInfo ici in ImageCodecInfo.GetImageEncoders())
				if (ici.FormatDescription == "JPEG")
				{
					jpegencoder = ici;
					break;
				}
			if (jpegencoder == null)
				return null;
			try
			{
				image.Save(stream, jpegencoder, ep);
			}
			catch (Exception)
			{
				return null;
			}
			stream.Seek(0, SeekOrigin.Begin);
			return stream;
		}

		/// <summary>
		/// Render a text message in an image
		/// </summary>
		/// <remarks>
		/// This method renders a text message in an image and returns a stream containing
		/// the image in JPEG format.
		/// </remarks>
		/// <param name="message">The message to render</param>
		/// <param name="format">The requested size of the resulting image</param>
		/// <returns>A stream containing the image or <c>null</c> of no matching JPEG encoder
		/// was found or an error occurred.</returns>
		public static Stream RenderMessage(string message, ImageSize format)
		{
			switch (format)
			{
				case ImageSize.Full:
				case ImageSize.Medium:
					return RenderMessage(message, 320, 240);
				case ImageSize.Thumbnail:
				default:
					return RenderMessage(message, 96, 72);
			}
		}

		private Field[] ConditionFields(SearchCondition condition)
		{
			SearchField restriction = condition.FieldRestriction();
			if (restriction == null)
			{
				ArrayList r = new ArrayList();
				foreach (Field f in GetFields())
					if (f.KeywordSearchable)
						r.Add(f);
				if (r.Count > 0)
					return (Field[])r.ToArray(typeof(Field));
			}
			else
			{
				return GetFields(restriction.GetFieldName(), true, restriction.IsDC());
			}
			return null;		
		}

		private int[] ConditionFieldIDs(SearchCondition condition)
		{
			Field[] fields = ConditionFields(condition);
			if (fields != null)
			{
				int[] fieldids = new int[fields.Length];
				for (int i = 0; i < fields.Length; i++)
					fieldids[i] = fields[i].ID;
				return fieldids;
			}
			return null;		
		}

		internal string[] ResolveFieldName(SearchCondition condition)
		{
			Field[] fields = ConditionFields(condition);
			if (fields != null)
			{
				string[] names = new string[fields.Length];
				for (int i = 0; i < fields.Length; i++)
					names[i] = fields[i].Name;
				return names;
			}
			return null;
		}
		
		internal ImageIdentifier[] Search(SearchCondition[] conditions, SearchEngine engine, ImageIdentifier[] searchwithin)
		{
			ImageIdentifier[] searchresult = Search(conditions, engine);
			if (searchwithin == null || searchresult == null)
				return searchresult;

			Hashtable within = new Hashtable(searchwithin.Length);
			foreach (ImageIdentifier id in searchwithin)
				if (!within.ContainsKey(id))
					within.Add(id, null);

			ArrayList filteredresult = new ArrayList();
			foreach (ImageIdentifier id in searchresult)
				if (within.ContainsKey(id))
					filteredresult.Add(id);

			return (ImageIdentifier[])filteredresult.ToArray(typeof(ImageIdentifier));
		}

		internal virtual ImageIdentifier[] Search(SearchCondition[] conditions, SearchEngine engine)
		{
			// create additional search condition to find only official collection images
			OwnerCondition ownercond = new OwnerCondition();
			ownercond.FindPublic = true;
			ownercond.FindOthers = false;
			ownercond.FindOwn = false;
			SearchCondition[] myconditions = 
				new SearchCondition[(conditions == null ? 1 : conditions.Length + 1)];
			if (conditions != null)
				conditions.CopyTo(myconditions, 1);
			myconditions[0] = ownercond;
			return this.PerformSearch(myconditions, engine);
		}

		/// <summary>
		/// Search this catalog
		/// </summary>
		/// <remarks>
		/// This internal method is called by the <see cref="Search(SearchCondition[], SearchEngine)"/> class on every collection
		/// included in a search, and by virtual collections. It should never be called directly.
		/// </remarks>
		/// <param name="conditions">An array of conditions</param>
		/// <param name="engine">The search engine to use for this search</param>
		/// <returns>An array of image identifiers matching the criteria, in no particular order</returns>
		internal ImageIdentifier[] PerformSearch(SearchCondition[] conditions, SearchEngine engine)
		{
			int[] images = PerformRawSearch(conditions, engine);

			// convert to array of ImageIdentifier
			if (images != null)
			{
				ImageIdentifier[] result = new ImageIdentifier[images.Length];
				for (int i = 0; i < images.Length; i++)
				{
					result[i].ID = images[i];
					result[i].CollectionID = id;
				}
				return result;
			}
			else
				return null;
		}

		/// <summary>
		/// Search this catalog
		/// </summary>
		/// <remarks>
        /// This internal method is called by the <see cref="Search(SearchCondition[], SearchEngine)"/> class on every collection
		/// included in a search, and by virtual collections. It should never be called directly.
		/// </remarks>
		/// <param name="conditions">An array of conditions</param>
		/// <param name="engine">The search engine to use for this search</param>
		/// <returns>An array of integers representing the image identifiers (without collection
		/// identifier information) matching the criteria, in no particular order</returns>		
		internal int[] PerformRawSearch(SearchCondition[] conditions, SearchEngine engine)
		{
			User.RequirePrivilege(Privilege.ReadCollection, this);
			if (conditions == null || conditions.Length == 0)
				return null;

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

			// split search conditions according to which search engine they should
			// be run against

			ArrayList luceneconds = new ArrayList();
			ArrayList databaseconds = new ArrayList();

			foreach (SearchCondition cond in conditions)
			{
				if ((cond.SearchEngine == SearchEngine.Default && uselucene) ||
					cond.SearchEngine == SearchEngine.Lucene)
					luceneconds.Add(cond);
				else
					databaseconds.Add(cond);
			}

			int[] luceneimages = null;
			int[] databaseimages = null;

			if (luceneconds.Count > 0)
			{
				luceneimages = PerformSearchWithLucene(
					(SearchCondition[])luceneconds.ToArray(typeof(SearchCondition)));

				if (databaseconds.Count == 0)
					return luceneimages;
			}

			if (databaseconds.Count > 0)
			{
				databaseimages = PerformSearchWithSQL(
					(SearchCondition[])databaseconds.ToArray(typeof(SearchCondition)));

				if (luceneconds.Count == 0)
					return databaseimages;
			}

			return Util.IntegerListTools.Intersect(luceneimages, databaseimages);

//			if ((engine == SearchEngine.Default && uselucene) || engine == SearchEngine.Lucene)
//				images = PerformSearchWithLucene(conditions);
//			else
//				images = PerformSearchWithSQL(conditions);
//
//			return images;
		}

		private int[] PerformSearchWithLucene(SearchCondition[] conditions)
		{
			ArrayList c = new ArrayList();
			foreach (SearchCondition sc in conditions)
			{
				string cond = sc.GetLuceneCondition(this);
				if (cond == null)
					return null;
				c.Add(cond);
			}

			string searchquery = String.Format("_coll:{0} AND {1}",
				FullTextIndex.Number.ToIndex(this.id),
				String.Join(" AND ", (string[])c.ToArray(typeof(string))));

			int[] images = null;

			FullTextIndex index = new FullTextIndex(this);
			IndexReader reader = null;
			IndexSearcher searcher = null;
			try
			{
				reader = index.GetIndexReader();
				if (reader != null)
				{	
					searcher = new IndexSearcher(reader);
					QueryParser parser = new QueryParser("_all", new StandardAnalyzer());
					
					Lucene.Net.Search.Query lucenequery;
					try
					{
						lucenequery = parser.Parse(searchquery);
					}
					catch (Lucene.Net.QueryParsers.ParseException e)
					{
						TransactionLog.Instance.AddException("Lucene exception", 
							"Query: " + searchquery, e);
						throw new CoreException("Could not parse search parameters");
					}
					
					Hits hits = searcher.Search(lucenequery);
					int hitcount = hits.Length();
					if (hitcount > 0)
					{
						images = new int[hitcount];
						for (int i = 0; i < hitcount; i++) 
						{
							Document doc = hits.Doc(i);
							images[i] = ImageIdentifier.Parse(doc.Get("_id")).ID;
						}
					}
				}
				else
					TransactionLog.Instance.Add("Full-text index missing",
						String.Format("The full-text index for collection {0} ('{1}') appears to be missing or is unavailable",
						this.id, this.Title));
			}
			finally
			{
				if (searcher != null)
					searcher.Close();
				if (reader != null)
					reader.Close();
			}

 			if (images != null)
			{
				using (DBConnection conn = DBConnector.GetConnection())
				{
					// restrict images based on share status
					User user = User.CurrentUser();
					Query query = new Query(conn,
						@"SELECT DISTINCT ID FROM Images WHERE 
					CollectionID={collectionid} AND ((UserID=0) OR (UserID IS NULL) 
					OR (Flags&{flags}={flags}) OR (UserID={userid}))");
					query.AddParam("collectionid", id);
					query.AddParam("flags", ImageFlag.Shared);
					query.AddParam("userid", (user != null ? user.ID : 0));
					images = IntegerListTools.Intersect(images, conn.TableToArray(conn.SelectQuery(query)));
				}
			}
			return images;
		}

		private int[] PerformSearchWithSQL(SearchCondition[] conditions)
		{
			User user = User.CurrentUser();
			int userid = (user != null ? user.ID : 0);

//			int[] matches = new int[conditions.Length];
			int[] images = null;
			Query query;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				// only need to count matches if there is more than one condition
/*				if (conditions.Length > 1)
					for (int i = 0; i < conditions.Length; i++)
					{
						matches[i] = 0;
						if (conditions[i].SearchOnImageTable())
						{
							try
							{
								query = new Query(conn,
									@"SELECT COUNT(DISTINCT ID) FROM Images 
									WHERE (CollectionID={collectionid}) AND (" +
									conditions[i].WhereCondition() + ") AND " +
									"((UserID=0) OR (UserID IS NULL) OR " +
									"(Flags&{flags}={flags}) OR (UserID={userid}))");
								query.AddParam("flags", ImageFlag.Shared);
								query.AddParam("userid", userid);
								query.AddParam("collectionid", id);
								matches[i] = conn.DataToInt(conn.ExecScalar(query), 0);
							}
							catch (Exception e) 
							{
								TransactionLog.Instance.AddException("Handled exception", e);
								matches[i] = 0;
								throw new CoreException(String.Format(
									"Problem with search condition: {0}", conditions[i].ToString()));
							}
						}
						else
						{
							int[] fieldids = ConditionFieldIDs(conditions[i]);
							// any fields match for this search condition?
							if (fieldids != null)
							{
								try
								{
									query = new Query(conn,
										@"SELECT COUNT(DISTINCT ImageID) FROM " + 
										"FieldData INNER JOIN Images " +
										"ON (FieldData.ImageID=Images.ID AND FieldData.CollectionID=Images.CollectionID) " +
                                        @"WHERE (FieldID IN {fieldids}) AND 
										(FieldData.CollectionID={collectionid}) AND (" +
										conditions[i].WhereCondition() + ") AND " +
										"((UserID=0) OR (UserID IS NULL) OR " +
										"(Flags&{flags}={flags}) OR (UserID={userid}))");
									query.AddParam("flags", ImageFlag.Shared);
									query.AddParam("userid", userid);
									query.AddParam("fieldids", fieldids);
									query.AddParam("collectionid", id);
									matches[i] = conn.DataToInt(conn.ExecScalar(query), 0);
								}
								catch (Exception e) 
								{
									TransactionLog.Instance.AddException("Handled exception", e);
									matches[i] = 0;
									throw new CoreException(String.Format(
										"Problem with search condition: {0}", conditions[i].ToString()));
								}
							}
						}
						if (matches[i] == 0)
							return null;
					}
*/				// run the conditions in increasing order of matches
				for (int i = 0; i < conditions.Length; i++)
				{
/*					// find the lowest number of matches
					int minidx = 0;
					for (int j = 0; j < conditions.Length; j++)
						if (matches[j] < matches[minidx])
							minidx = j;
					// mark that index of as used by setting the number of matches to maximum value
					matches[minidx] = Int32.MaxValue;
*/
					int minidx = i;
					// run search
					string sql;
					int[] fieldids = null;
					if (conditions[minidx].SearchOnImageTable())
					{
						sql = @"SELECT DISTINCT ID AS ImageID FROM Images 
							WHERE (CollectionID={collectionid}) AND (" +
							conditions[minidx].WhereCondition() + ")";
					}
					else
					{
						fieldids = ConditionFieldIDs(conditions[minidx]);
						// any fields match for this search condition?
						if (fieldids != null)
						{
							sql = "SELECT DISTINCT ImageID FROM " +
								"FieldData INNER JOIN Images " +
								"ON (FieldData.ImageID=Images.ID AND FieldData.CollectionID=Images.CollectionID) " +
								"WHERE (FieldID IN {fieldids}) AND (FieldData.CollectionID={collectionid}) AND (" + 
								conditions[minidx].WhereCondition() + ")";
						}
						else
							// no fields available to search, so no results
							return null;
					}

					sql += " AND ((UserID=0) OR (UserID IS NULL) OR " +
						"(Flags&{flags}={flags}) OR (UserID={userid}))";
					try
					{
						query = new Query(conn, sql);
						query.AddParam("collectionid", id);
						query.AddParam("fieldids", fieldids);
						query.AddParam("flags", ImageFlag.Shared);
						query.AddParam("userid", userid);
						int[] result = conn.TableToArray(conn.SelectQuery(query));
						images = (images != null ? IntegerListTools.Intersect(result, images) : result);
						if (images == null)
							return null;
					}
					catch (Exception e)
					{
						TransactionLog.Instance.AddException("Handled exception", 
							String.Format("SQL statement: {0}", sql), e);
						throw new CoreException(String.Format(
							"Problem with search condition: {0}", conditions[minidx].ToString()));
					}
				}	
			}		
			return images;
		}

		/// <summary>
		/// Image Settings
		/// </summary>
		/// <remarks>
		/// Every collection has individual settings for the image resources associated with images
		/// in that collection.  Each collection has several instances of the ImageSettings structure
		/// to hold the settings for each of the available image sizes (thumbnails, full, etc.)
		/// </remarks>
		/// <param name="size">The image size to return the image settings for</param>
		/// <returns>Image settings for the requested image size</returns>
		public ImageSettings GetImageSettings(ImageSize size)
		{
			switch (size)
			{
				case ImageSize.Full:
					return fullImageSettings;
				case ImageSize.Medium:
					return mediumImageSettings;
				case ImageSize.Thumbnail:
					return thumbImageSettings;
				default:
					throw new CoreException("Invalid image size specification");
			}
		}

		/// <summary>
		/// Image Settings
		/// </summary>
		/// <remarks>
		/// Every collection has individual settings for the image resources associated with images
		/// in that collection.  Each collection has several instances of the ImageSettings structure
		/// to hold the settings for each of the available image sizes (thumbnails, full, etc.)
		/// </remarks>
		/// <param name="settings">Image settings for the requested image size</param>
		/// <param name="size">The image size to set the image settings for</param>
		public void SetImageSettings(ImageSize size, ImageSettings settings)
		{
			MarkModified();
			switch (size)
			{
				case ImageSize.Full:
					fullImageSettings = settings;
					break;
				case ImageSize.Medium:
					mediumImageSettings = settings;
					break;
				case ImageSize.Thumbnail:
					thumbImageSettings = settings;
					break;
				default:
					throw new CoreException("Invalid image size specification");
			}
		}

		/// <summary>
		/// Create Dublin Core fields
		/// </summary>
		/// <remarks>
		/// This method creates the Dublin Core fields in this collection.  The collection must
		/// not contain any fields and must have been saved to the database.
		/// </remarks>
		public void CreateDublinCoreFields()
		{
			if (this.fields.Count > 0)
				throw new CoreException("Cannot create Dublin Core fields on non-empty collections.");
			if (this.id == 0)
				throw new CoreException("Cannot create Dublin Core fields on newly created collections.");
	
			Field field;

			field = new Field();
			field.Label = "Title";
			field.Name = "Title";
			field.DCElement = "Title";
			field.Type = FieldType.Text;
			field.Searchable = true;
			field.KeywordSearchable = true;
			field.Sortable = true;
			field.Browsable = true;
			field.ShortView = DisplayMode.All;
			field.MediumView = DisplayMode.All;
			field.LongView = DisplayMode.All;
			AddField(field);

			field = new Field();
			field.Label = "Creator";
			field.Name = "Creator";
			field.DCElement = "Creator";
			field.Type = FieldType.Text;
			field.Searchable = true;
			field.KeywordSearchable = true;
			field.Sortable = true;
			field.Browsable = true;
			field.ShortView = DisplayMode.All;
			field.MediumView = DisplayMode.All;
			field.LongView = DisplayMode.All;
			AddField(field);

			field = new Field();
			field.Label = "Subject";
			field.Name = "Subject";
			field.DCElement = "Subject";
			field.Type = FieldType.Text;
			field.Searchable = true;
			field.KeywordSearchable = true;
			field.Sortable = true;
			field.Browsable = true;
			field.ShortView = DisplayMode.All;
			field.MediumView = DisplayMode.All;
			field.LongView = DisplayMode.All;
			AddField(field);

			field = new Field();
			field.Label = "Description";
			field.Name = "Description";
			field.DCElement = "Description";
			field.Type = FieldType.Text;
			field.Searchable = true;
			field.KeywordSearchable = true;
			field.Sortable = true;
			field.Browsable = true;
			field.ShortView = DisplayMode.All;
			field.MediumView = DisplayMode.All;
			field.LongView = DisplayMode.All;
			AddField(field);

			field = new Field();
			field.Label = "Publisher";
			field.Name = "Publisher";
			field.DCElement = "Publisher";
			field.Type = FieldType.Text;
			field.Searchable = true;
			field.KeywordSearchable = true;
			field.Sortable = true;
			field.Browsable = true;
			field.ShortView = DisplayMode.None;
			field.MediumView = DisplayMode.All;
			field.LongView = DisplayMode.All;
			AddField(field);

			field = new Field();
			field.Label = "Contributor";
			field.Name = "Contributor";
			field.DCElement = "Contributor";
			field.Type = FieldType.Text;
			field.Searchable = true;
			field.KeywordSearchable = true;
			field.Sortable = true;
			field.Browsable = true;
			field.ShortView = DisplayMode.None;
			field.MediumView = DisplayMode.All;
			field.LongView = DisplayMode.All;
			AddField(field);

			field = new Field();
			field.Label = "Date";
			field.Name = "Date";
			field.DCElement = "Date";
			field.Type = FieldType.Date;
			field.Searchable = true;
			field.KeywordSearchable = false;
			field.Sortable = true;
			field.Browsable = false;
			field.ShortView = DisplayMode.All;
			field.MediumView = DisplayMode.All;
			field.LongView = DisplayMode.All;
			AddField(field);

			field = new Field();
			field.Label = "Type";
			field.Name = "Type";
			field.DCElement = "Type";
			field.Type = FieldType.Text;
			field.Searchable = true;
			field.KeywordSearchable = true;
			field.Sortable = true;
			field.Browsable = true;
			field.ShortView = DisplayMode.None;
			field.MediumView = DisplayMode.All;
			field.LongView = DisplayMode.All;
			AddField(field);

			field = new Field();
			field.Label = "Format";
			field.Name = "Format";
			field.DCElement = "Format";
			field.Type = FieldType.Text;
			field.Searchable = true;
			field.KeywordSearchable = true;
			field.Sortable = true;
			field.Browsable = true;
			field.ShortView = DisplayMode.None;
			field.MediumView = DisplayMode.All;
			field.LongView = DisplayMode.All;
			AddField(field);

			field = new Field();
			field.Label = "Identifier";
			field.Name = "Identifier";
			field.DCElement = "Identifier";
			field.Type = FieldType.Text;
			field.Searchable = true;
			field.KeywordSearchable = true;
			field.Sortable = true;
			field.Browsable = true;
			field.ShortView = DisplayMode.All;
			field.MediumView = DisplayMode.All;
			field.LongView = DisplayMode.All;
			AddField(field);

			field = new Field();
			field.Label = "Source";
			field.Name = "Source";
			field.DCElement = "Source";
			field.Type = FieldType.Text;
			field.Searchable = true;
			field.KeywordSearchable = true;
			field.Sortable = true;
			field.Browsable = true;
			field.ShortView = DisplayMode.None;
			field.MediumView = DisplayMode.All;
			field.LongView = DisplayMode.All;
			AddField(field);

			field = new Field();
			field.Label = "Language";
			field.Name = "Language";
			field.DCElement = "Language";
			field.Type = FieldType.Text;
			field.Searchable = true;
			field.KeywordSearchable = true;
			field.Sortable = true;
			field.Browsable = true;
			field.ShortView = DisplayMode.None;
			field.MediumView = DisplayMode.None;
			field.LongView = DisplayMode.All;
			AddField(field);

			field = new Field();
			field.Label = "Relation";
			field.Name = "Relation";
			field.DCElement = "Relation";
			field.Type = FieldType.Text;
			field.Searchable = true;
			field.KeywordSearchable = true;
			field.Sortable = true;
			field.Browsable = true;
			field.ShortView = DisplayMode.None;
			field.MediumView = DisplayMode.None;
			field.LongView = DisplayMode.All;
			AddField(field);

			field = new Field();
			field.Label = "Coverage";
			field.Name = "Coverage";
			field.DCElement = "Coverage";
			field.Type = FieldType.Text;
			field.Searchable = true;
			field.KeywordSearchable = true;
			field.Sortable = true;
			field.Browsable = true;
			field.ShortView = DisplayMode.None;
			field.MediumView = DisplayMode.None;
			field.LongView = DisplayMode.All;
			AddField(field);

			field = new Field();
			field.Label = "Rights";
			field.Name = "Rights";
			field.DCElement = "Rights";
			field.Type = FieldType.Text;
			field.Searchable = true;
			field.KeywordSearchable = true;
			field.Sortable = false;
			field.Browsable = false;
			field.ShortView = DisplayMode.None;
			field.MediumView = DisplayMode.None;
			field.LongView = DisplayMode.All;
			AddField(field);
		}

		/// <summary>
		/// Split list of image identifiers by collection
		/// </summary>
		/// <remarks>
		/// This is a utility function.
		/// </remarks>
		/// <param name="images">A list of image identifiers</param>
		/// <returns>A hashtable with collection identifiers as keys and ArrayLists
		/// containing image identifiers as values.</returns>
		public static Hashtable SeperateImageIdsByCollection(ICollection images)
		{
			Hashtable result = new Hashtable();
			foreach (ImageIdentifier id in images)
			{
				if (result.ContainsKey(id.CollectionID))
					((ArrayList)result[id.CollectionID]).Add(id);
				else
				{
					ArrayList a = new ArrayList();
					a.Add(id);
					result[id.CollectionID] = a;
				}
			}
			return result;
		}

		/// <summary>
		/// Create a new share entry
		/// </summary>
		/// <remarks>
		/// Collection share entries are used to controll remote access to a collection.
		/// </remarks>
		/// <returns>A new collection share entry</returns>
		public CollectionShareEntry CreateShareEntry()
		{
			return new CollectionShareEntry(this.id);
		}

		/// <summary>
		/// List of share entries
		/// </summary>
		/// <remarks>
		/// Retrieves a complete list of collection share entries for this collection.  The list
		/// may be empty, but will not be <c>null</c>.
		/// </remarks>
		/// <returns>An array of collection share entries for this collection</returns>
		public CollectionShareEntry[] GetShareEntries()
		{
			return (CollectionShareEntry[])CollectionShareEntry.GetForCollection(this.id).ToArray(typeof(CollectionShareEntry));
		}

		/// <summary>
		/// Find a valid share entry
		/// </summary>
		/// <remarks>
		/// This method uses the current active user to find a collection share entry that
		/// matches the user's identity and IP address.
		/// </remarks>
		/// <returns>A collection share entry for the current user or <c>null</c> if no such
		/// entry was found.</returns>
		public CollectionShareEntry FindValidShareEntry()
		{
			User user = User.CurrentUser();
			if (user != null || user.IPAddress != null)
				foreach (CollectionShareEntry entry in GetShareEntries())
					if (entry.UserID == user.ID && 
						IPAddress.IsInSubnet(user.IPAddress, entry.Subnet, entry.Mask))
						return entry;
			return null;
		}
	}
}
