using System;
using System.Collections;
using System.Data;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Caching;
using System.Net;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Orciid.Core.localhost;

namespace Orciid.Core
{
	/// <summary>
	/// Remote collection interface
	/// </summary>
	/// <remarks>
	/// A list of methods and properties each remote collection has to implement.
	/// </remarks>
	public interface IRemoteCollection
	{
		/// <summary>
		/// Precache an image
		/// </summary>
		/// <remarks>
		/// This method is called whenever an image is expired and needs to be updated.
		/// </remarks>
		/// <param name="image">The image to precache</param>
		void PrecacheImage(Image image);
	}
	
	/// <summary>
	/// Remote collection
	/// </summary>
	/// <remarks>
	/// A remote collection is a virtual collection that refers to a collection
	/// on this or another MDID system
	/// </remarks>
	public class RemoteCollection:
		Collection, IRemoteCollection	
	{

		/// <summary>
		/// URL to remote collection
		/// </summary>
		protected string remoteurl;  

		/// <summary>
		/// Default constructor
		/// </summary>
		/// <remarks>
		/// Default constructor
		/// </remarks>
		public RemoteCollection():
			base()
		{
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Internal constructor
		/// </remarks>
		internal RemoteCollection(bool init):
			base(init)
		{
		}

		static RemoteCollection()
		{
			ScheduleRemoteCollectionCacheUpdateTask(TimeSpan.Zero);
		}

		private static void ScheduleRemoteCollectionCacheUpdateTask(TimeSpan wait)
		{
			// schedule cache refresh for all remote collections
			BackgroundTask task = new BackgroundTask(
				"Scheduling Remote Collection Cache Updates", 
				-1, 
				"scheduleremotecacheupdate",
				DateTime.Now + wait);
			task.OnRun += new BackgroundTaskDelegate(ScheduleRemoteCollectionCacheUpdates);
			task.Queue();
		}

		private static bool ScheduleRemoteCollectionCacheUpdates(BackgroundTask task, object info)
		{
			try
			{
				foreach (int cid in Collection.GetCollectionIDs())
				{
					Collection coll = Collection.GetByID(false, cid);
					if (coll != null && coll is RemoteCollection)
					{
						BackgroundTask ctask = new BackgroundTask(
							String.Format("Updating Remote Collection {0} cache", coll.ID),
							-2,
							"remotecacheupdate");
						ctask.OnRun += new BackgroundTaskDelegate(((RemoteCollection)coll).UpdateCachedImages);
						ctask.Queue();
					}
				}
			}
			finally
			{
				int minutes = 60;
				if (Configuration.Instance.ContainsKey("content.remotecollections.updatecache"))
					minutes = Configuration.Instance.GetInt("content.remotecollections.updatecache");
				ScheduleRemoteCollectionCacheUpdateTask(TimeSpan.FromMinutes(minutes)); 
			}
			return true;
		}

		private static Hashtable servicecache = new Hashtable();

		private class ServiceInfo
		{
			public string username;
			public string password;
			public int collectionid;
			public RemoteCollectionService service;
			public string remoteurl;
			public DateTime lastlogin = DateTime.MinValue;
		}

		/// <summary>
		/// Object initializer
		/// </summary>
		/// <remarks>
		/// This method is called after the collection object is retrieved from the database.
		/// For the RemoteCollection class, it retrieves the RemoteURL from the database.
		/// </remarks>
		/// <param name="conn">An open database connection</param>
		protected override void InitializeAdditionalFields(DBConnection conn)
		{
			Query query = new Query(conn,
				@"SELECT RemoteURL FROM Collections WHERE ID={id}");
			query.AddParam("id", id);
			remoteurl = conn.DataToString(conn.ExecScalar(query));
		}

		/// <summary>
		/// Service information
		/// </summary>
		/// <remarks>
		/// Each remote collection keeps track of service information, e.g. information
		/// for the current login session on the remote server.
		/// </remarks>
		/// <returns></returns>
		protected virtual object GetServiceInfo()
		{
			ServiceInfo info;
			lock (servicecache)
			{
				info = (ServiceInfo)servicecache[this.id];
				if (info == null)
				{
					Regex regex = new Regex(
						@"^(?'prot'https?://)(?'user'[^:]+):(?'pwd'[^@]+)@(?'svc'.+),(?'cid'\d+)$");
					Match match = regex.Match(remoteurl);
					if (!match.Success)
						throw new CoreException("Invalid remote URL");
					info = new ServiceInfo();
					info.service = new RemoteCollectionService();
					info.remoteurl = match.Groups["prot"].Value + match.Groups["svc"].Value;
					info.remoteurl += (info.remoteurl.EndsWith("/") ? "" : "/");
					info.service.Url = info.remoteurl + "WebServices/RemoteCollection.asmx";
					info.service.Timeout = 30 * 1000; // 30 seconds
					info.service.CookieContainer = new CookieContainer();
					info.username = match.Groups["user"].Value;
					info.password = match.Groups["pwd"].Value;
					info.collectionid = Int32.Parse(match.Groups["cid"].Value);
					servicecache[this.id] = info;
				}
				if (info.lastlogin + TimeSpan.FromMinutes(10) < DateTime.Now)
				{
					// Log into remote service
					if (!info.service.Login(info.username, info.password, info.collectionid))
						throw new CoreException("RemoteCollection login failed");
					info.lastlogin = DateTime.Now;
					// Update collection structure
					CollectionInfo cinfo = info.service.GetCollectionInfo();
					UpdateCollectionFields(cinfo.field);
				}
			}
			return info;
		}

		private void UpdateCollectionFields(FieldInfo[] remotefields)
		{
			bool foundchange = false;
			// check for new fields
			foreach (FieldInfo rf in remotefields)
			{
				Field f = this.GetField(rf.name);
				if (f != null)
				{
					if (f.Label != rf.label)
						f.Label = rf.label;
					// cannot handle controlled list yet, so ignore difference in that field type
					if (f.Type != (FieldType)rf.type && 
						(f.Type != FieldType.Text || (FieldType)rf.type != FieldType.ControlledList))
						f.Type = ((FieldType)rf.type == FieldType.Text || 
							(FieldType)rf.type == FieldType.ExactText ||
							(FieldType)rf.type == FieldType.Date ? (FieldType)rf.type : FieldType.Text);
					if (f.DCElement != rf.dcelement)
						f.DCElement = rf.dcelement;
					if (f.DCRefinement != rf.dcrefinement)
						f.DCRefinement = rf.dcrefinement;
					if (f.Searchable != rf.searchable)
						f.Searchable = rf.searchable;
					if (f.KeywordSearchable != rf.keywordsearchable)
						f.KeywordSearchable = rf.keywordsearchable;
					if (f.Sortable != rf.sortable)
						f.Sortable = rf.sortable;
					if (f.Browsable != rf.browsable)
						f.Browsable = rf.browsable;
					if (f.ShortView != (DisplayMode)rf.shortview)
						f.ShortView = (DisplayMode)rf.shortview;
					if (f.MediumView != (DisplayMode)rf.mediumview)
						f.MediumView = (DisplayMode)rf.mediumview;
					if (f.LongView != (DisplayMode)rf.longview)
						f.LongView = (DisplayMode)rf.longview;
					if (f.Modified)
						f.Update();
				}
				else
				{
					foundchange = true;
					f = new Field();
					f.Name = rf.name;
					f.Label = rf.label;
					// cannot handle controlled list yet
					f.Type = ((FieldType)rf.type == FieldType.Text || 
						(FieldType)rf.type == FieldType.ExactText ||
						(FieldType)rf.type == FieldType.Date ? (FieldType)rf.type : FieldType.Text);
					f.DCElement = rf.dcelement;
					f.DCRefinement = rf.dcrefinement;
					f.Searchable = rf.searchable;
					f.KeywordSearchable = rf.keywordsearchable;
					f.Sortable = rf.sortable;
					f.Browsable = rf.browsable;
					f.ShortView = (DisplayMode)rf.shortview;
					f.MediumView = (DisplayMode)rf.mediumview;
					f.LongView = (DisplayMode)rf.longview;
					this.AddField(f);
				}
			}
			// check for fields that no longer exist
			Field[] localfields = this.GetFields();
			for (int i = 0; i < localfields.Length; i++)
			{
				bool found = false;
				foreach (FieldInfo rf in remotefields)
					if (localfields[i].Name == rf.name)
					{
						found = true;
						break;
					}
				if (!found)
				{
					this.RemoveField(localfields[i]);
					foundchange = true;
				}
			}
			// check for field order
			localfields = this.GetFields();
			for (int i = 0; i < remotefields.Length && i < localfields.Length; i++)
			{
				if (localfields[i].Name != remotefields[i].name)
				{
					for (int j = i + 1; j < remotefields.Length; j++)
					{
						if (localfields[j].Name == remotefields[i].name)
						{
							this.MoveField(localfields[j], localfields[i]);
							localfields = this.GetFields();
							break;
						}
					}
				}
			}

			if (foundchange)
			{
				// update CachedUntil setting to force record update on next cache check
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"UPDATE Images SET CachedUntil={cacheduntil} WHERE CollectionID={id}");
					query.AddParam("id", this.id);
					query.AddParam("cacheduntil", DateTime.Now);
					conn.ExecQuery(query);
				}
			}
		}

		/// <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 override ArrayList GetImagesByID(bool enforcepriv, ArrayList imageids)
		{
			return GetImagesByID(enforcepriv, imageids, true);
		}

		/// <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>
		/// <param name="complete">A boolean indicating if image stubs are acceptable or not.
		/// If <c>true</c>, only complete image records will be returned, which may cause a 
		/// delay while image stubs are loaded.</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 ArrayList GetImagesByID(bool enforcepriv, ArrayList imageids, bool complete)
		{
			if (complete)
				CacheStubImageRecords(imageids);
			return base.GetImagesByID(enforcepriv, imageids);
		}

		/// <summary>
		/// Finish downloading image record stubs
		/// </summary>
		/// <remarks>
		/// This method retrieves and populates the complete image
		/// record for all existing stubs.  A regular remote collection
		/// never creates image stubs, but derived collection types may do so.
		/// </remarks>
		/// <param name="imageids">A list of image records to complete if they
		/// are still stubs.</param>
		public virtual void CacheStubImageRecords(ArrayList imageids)
		{
			// a regular remote collection does not create image stubs,
			// so no action required here
		}

		/// <summary>
		/// Remote URL
		/// </summary>
		/// <remarks>
		/// A URL with all information required to connect to a collection
		/// on a remote server
		/// </remarks>
		/// <value>
		/// The remote URL for this collection
		/// </value>
		public string RemoteUrl
		{
			get
			{
				return remoteurl;
			}
			set
			{
				MarkModified();
				if (value != null && value.Length > 255)
					throw new CoreException("Maximum remote URL length is 255 characters");
				remoteurl = (value != null && value.Length == 0 ? null : value);
				
				// clear service cache
				servicecache = new Hashtable();
			}
		}

		/// <summary>
		/// Remote server
		/// </summary>
		/// <remarks>
		/// Extracts the server information from the remote URL
		/// </remarks>
		/// <value>
		/// The remote server for this collection
		/// </value>
		public virtual string RemoteServer
		{
			get
			{
				Regex regex = new Regex(
					@"^(?'prot'https?://)(?'user'[^:]+):(?'pwd'[^@]+)@(?'svc'.+),(?'cid'\d+)$");
				Match match = regex.Match(remoteurl);
				if (!match.Success)
					throw new CoreException("Invalid remote URL");
				return match.Groups["prot"].Value + match.Groups["svc"].Value;
			}
		}

		/// <summary>
		/// Get the number of images in current user's favorites collection
		/// </summary>
		/// <remarks>
		/// This method returns the number of images in the favorites collection of
		/// the current user; not the total number of images for all users.
		/// </remarks>
		/// <value>The number of images</value>
		public override int ImageCount
		{
			get
			{
				try
				{
					ServiceInfo info = (ServiceInfo)GetServiceInfo();
					return info.service.GetImageCount();
				}
				catch (Exception ex)
				{
					TransactionLog.Instance.AddException("ImageCount failed", 
						"Could not retrieve image count for remote collection " + this.id.ToString(),
						ex);
					return -1;
				}
			}
		}

		/// <summary>
		/// Checks for browsable fields
		/// </summary>
		/// <remarks>
		/// Remote collections are not browsable.
		/// </remarks>
		/// <value><c>false</c></value>
		public override bool IsBrowsable
		{
			get
			{
				return false;
			}
		}

		/// <summary>
		/// Check Modification Privilege
		/// </summary>
		/// <remarks>
		/// Since this class represents a virtual collection, it cannot be modified.
		/// </remarks>
		/// <param name="user">The user object to check the privileges for</param>
		/// <returns><c>false</c></returns>
		public override bool CanModify(User user)
		{
			return true;
		}

		/// <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>false</c></returns>
		public override bool CanModifyImage(User user, Image image)
		{
			return false;
		}

		/// <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>false</c></returns>
		public override bool CanCreateImage(User user, bool personal)
		{
			return false;
		}

		/// <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,RemoteUrl) 
						VALUES ({title},{description},{resourcepath},{usageagreement},{type},
						{fullimageheight},{fullimagewidth},{fullimagequality},{fullimagefixedsize},{fullimagebgcolor},
						{mediumimageheight},{mediumimagewidth},{mediumimagequality},{mediumimagefixedsize},{mediumimagebgcolor},
						{thumbimageheight},{thumbimagewidth},{thumbimagequality},{thumbimagefixedsize},{thumbimagebgcolor},
						{groupid},{cacherestriction},{remoteurl})");
					query.AddParam("title", title);
					query.AddParam("description", description);
					query.AddParam("resourcepath", resourcepath.ToString());
					query.AddParam("usageagreement", usageagreement);
					query.AddParam("type", GetRemoteCollectionType());					
					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("remoteurl", remoteurl);
					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},RemoteUrl={remoteurl} 
						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("remoteurl", remoteurl);
					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>
		/// Collection type
		/// </summary>
		/// <remarks>
		/// Returns the collection type identifier, which is <c>R</c>.
		/// </remarks>
		/// <returns><c>R</c></returns>
		protected virtual char GetRemoteCollectionType()
		{
			return 'R';
		}

		internal override ImageIdentifier[] Search(SearchCondition[] conditions, SearchEngine engine)
		{
			try
			{
				ServiceInfo info = (ServiceInfo)GetServiceInfo();
				Orciid.Core.Search.SearchParameters sp =
					new Orciid.Core.Search.SearchParameters();
				foreach (SearchCondition cond in conditions)
					if (!(cond is OwnerCondition)) // don't support owner searches across installations
						sp.conditions.Add(cond);

				if (sp.conditions.Count == 0)
					return null;

				string xml = sp.ToXml();
				string[] result = info.service.Search(xml);

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

				using (DBConnection conn = DBConnector.GetConnection())
				{
					CacheImageRecords(conn, info, result);

					// build list of local image ids
					Query query = new Query(conn,
						@"SELECT ID FROM Images WHERE CollectionID={id} AND RemoteID IN {remoteids}");
					query.AddParam("id", this.id);
					query.AddParam("remoteids", result);
					int[] localids = conn.TableToArray(conn.SelectQuery(query));
					ImageIdentifier[] iids = new ImageIdentifier[localids.Length];
					for (int i = 0; i < localids.Length; i++)
						iids[i] = new ImageIdentifier(localids[i], this.id);
					return iids;
				}
			}
			catch (Exception ex)
			{
				TransactionLog.Instance.AddException("Remote search failed", 
					String.Format("CollectionID={0}", this.id),
					ex);
				return null;
			}
		}

		private DateTime OldestNonMinimumDate(params DateTime[] dates)
		{
			DateTime oldest = DateTime.MaxValue;
			foreach (DateTime d in dates)
				if (d != DateTime.MinValue && d < oldest)
					oldest = d;
			return oldest;
		}

		/// <summary>
		/// Cache image records
		/// </summary>
		/// <remarks>
		/// This method caches the specified image records
		/// </remarks>
		/// <param name="conn">An open database connection</param>
		/// <param name="oinfo">A valid service info object (see <see cref="GetServiceInfo"/>)</param>
		/// <param name="remoteids">The identifiers of the remote images to cache</param>
		protected virtual void CacheImageRecords(DBConnection conn, object oinfo, string[] remoteids)
		{
			ServiceInfo info = (ServiceInfo)oinfo;
			ArrayList missing = new ArrayList(remoteids);
			ArrayList update = new ArrayList();

			// find locally cached images
			// this may become a long SQL statement
			Query query = new Query(conn,
				@"SELECT ID,RemoteID,CachedUntil FROM Images 
				WHERE CollectionID={id} AND RemoteID IN {remoteids}");
			query.AddParam("id", this.id);
			query.AddParam("remoteids", remoteids);
			DataTable table = conn.SelectQuery(query);
		
			// create list of images not cached already or where cache expired
			foreach (DataRow row in table.Rows)
			{
				string rid = conn.DataToString(row["RemoteID"]);
				int lid = conn.DataToInt(row["ID"], 0);
				DateTime cacheduntil = conn.DataToDateTime(row["CachedUntil"]);
				if (cacheduntil > DateTime.Now)
					missing.Remove(rid);
				else
					update.Add(new ImageIdentifier(lid, this.id));
			}

			// download missing images
			if (missing.Count > 0)
			{
				ImageInfo[] infos = 
					info.service.GetImageInfo((string[])missing.ToArray(typeof(string)));
				if (infos != null)
				{
					ArrayList updatingimages = 
						(update.Count > 0 ? this.GetImagesByID(false, update) : null);
					foreach (ImageInfo iinfo in infos)
					{
						Image image = null;
						// are we updating an existing image?
						if (updatingimages != null)
							foreach (Image i in updatingimages)
								// yes, updating image
								if (i.remoteid == iinfo.id)
								{
									image = i;
									// check if cached image files need to be refreshed
									if (OldestNonMinimumDate(
										image.GetResourceModificationDate(ImageSize.Full),
										image.GetResourceModificationDate(ImageSize.Medium),
										image.GetResourceModificationDate(ImageSize.Thumbnail)) < iinfo.filedate)
									{
										Collection coll = Collection.GetByID(false, image.ID.CollectionID);
										if (coll != null)
											coll.DeleteImageResource(image.Resource);
										PrecacheImage(image);
									}
									break;
								}
						if (image == null)
							// no, create new image
							image = new Image(new ImageIdentifier(0, id), null);
						image.createddate = iinfo.created;
						image.modifieddate = iinfo.modified;
						image.cacheduntil = iinfo.cacheuntil;
						image.expires = iinfo.expire;
						image.remoteid = iinfo.id;
						// build field data records myself to circumvent access restrictions
						// this is not very elegant
						Hashtable datafields = new Hashtable();
						foreach (FieldDataInfo data in iinfo.fielddata)
						{
							FieldData fd = null;
							Field f = this.GetField(data.field);
							if (f != null)
							{
								if (datafields.ContainsKey(f))
									fd = (FieldData)datafields[f];
								else
								{
									fd = FieldData.CreateFieldData(null, f);
									datafields.Add(f, fd);
								}
							}
							if (fd != null)
								fd.Add(data.Value);
						}
						foreach (Field f in datafields.Keys)
							image.SetFieldData(f, (FieldData)datafields[f]);

						// update the image without checking access restrictions and with force update
						image.Update(false, true);
					}
				}
			}
		}

		private bool UpdateCachedImages(BackgroundTask task, object info)
		{
			string[] refresh = null;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				// get list of all images that are in use anywhere and that are past
				// their CachedUntil date
				Query query = new Query(conn,
				@"SELECT RemoteID FROM 
						(((Images 
						  LEFT JOIN Slides 
							ON Images.ID=Slides.ImageID
							AND Images.CollectionID=Slides.CollectionID) 
						  LEFT JOIN FavoriteImages 
							ON Images.ID=FavoriteImages.ImageID 
							AND Images.CollectionID=FavoriteImages.CollectionID) 
						  LEFT JOIN ImageAnnotations
							ON Images.ID=ImageAnnotations.ImageID
							AND Images.CollectionID=ImageAnnotations.CollectionID)
					WHERE Images.CollectionID={id} AND Images.CachedUntil<{cacheduntil} AND
					(Slides.ImageID IS NOT NULL 
					OR FavoriteImages.ImageID IS NOT NULL 
					OR ImageAnnotations.ImageID IS NOT NULL)");
				query.AddParam("id", this.id);
				query.AddParam("cacheduntil", DateTime.Now);
				DataTable table = conn.SelectQuery(query);

				if (table.Rows.Count > 0)
				{
					refresh = new string[table.Rows.Count];
					for (int i = 0; i < refresh.Length; i++)
						refresh[i] = conn.DataToString(table.Rows[i]["RemoteID"]);
				}

				// Refresh any records that are currently in use
				if (refresh != null)
				{
					CacheImageRecords(conn, GetServiceInfo(), refresh);
				}
			
				// Remove any records that are past their CachedUntil date and not in use,
				// or are past their Expires date
				query = new Query(conn,
					@"SELECT Images.ID FROM 
						(((Images 
						  LEFT JOIN Slides 
							ON Images.ID=Slides.ImageID
							AND Images.CollectionID=Slides.CollectionID) 
						  LEFT JOIN FavoriteImages 
							ON Images.ID=FavoriteImages.ImageID 
							AND Images.CollectionID=FavoriteImages.CollectionID) 
						  LEFT JOIN ImageAnnotations
							ON Images.ID=ImageAnnotations.ImageID
							AND Images.CollectionID=ImageAnnotations.CollectionID)
					WHERE Images.CollectionID={id} AND 
					((Images.CachedUntil<{now} AND
					Slides.ImageID IS NULL 
					AND FavoriteImages.ImageID IS NULL 
					AND ImageAnnotations.ImageID IS NULL) OR
					(Images.Expires<{now}))");
				query.AddParam("id", this.id);
				query.AddParam("now", DateTime.Now);
				int[] todelete = conn.TableToArray(conn.SelectQuery(query));

				if (todelete != null && todelete.Length > 0)
				{
					ArrayList ids = new ArrayList(todelete.Length);
					foreach (int iid in todelete)
						ids.Add(new ImageIdentifier(iid, this.id));
					ArrayList images = this.GetImagesByID(false, ids);
					if (images != null)
						foreach (Image image in images)
							image.Delete(false);
				}
			}
			CleanupCache();
			return true;
		}

		/// <summary>
		/// Clean up cache
		/// </summary>
		/// <remarks>
		/// This method removes image files that are expired or are currently not
		/// in use to reduce the cache size to below the limit.
		/// </remarks>
		protected virtual void CleanupCache()
		{
			Properties props = Properties.GetProperties(this);
			long CacheSizeLimit = (long)props.GetAsInt("cachesizelimit", 1024) * (long)1024 * (long)1024;

			// check image cache size
			long size = GetDirectorySize(new DirectoryInfo(resourcepath.PhysicalRoot));
			if (size > CacheSizeLimit)
			{
				// remove files until cache is smaller than limit
				// get files that can be removed
				ArrayList resources = new ArrayList();
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
					@"SELECT Resource FROM 
						(((Images 
						  LEFT JOIN Slides 
							ON Images.ID=Slides.ImageID
							AND Images.CollectionID=Slides.CollectionID) 
						  LEFT JOIN FavoriteImages 
							ON Images.ID=FavoriteImages.ImageID 
							AND Images.CollectionID=FavoriteImages.CollectionID) 
						  LEFT JOIN ImageAnnotations
							ON Images.ID=ImageAnnotations.ImageID
							AND Images.CollectionID=ImageAnnotations.CollectionID)
					WHERE Images.CollectionID={id} AND
					(Slides.ImageID IS NULL 
					AND FavoriteImages.ImageID IS NULL 
					AND ImageAnnotations.ImageID IS NULL)
					ORDER BY Expires");
					query.AddParam("id", this.id);
					DataTable table = conn.SelectQuery(query);

					foreach (DataRow row in table.Rows)
						resources.Add(conn.DataToString(row["Resource"]));
				}
				while (size > CacheSizeLimit && resources.Count > 0)
				{
					string resource = (string)resources[0];
					resources.RemoveAt(0);
					// cannot use this.GetResourceSize, since it would cause caching of the image
					// if it does not exist yet
					long fullsize = base.GetResourceSize(false, resource, ImageSize.Full);
					long mediumsize = base.GetResourceSize(false, resource, ImageSize.Medium);
					long thumbsize = base.GetResourceSize(false, resource, ImageSize.Thumbnail);
					long total = (fullsize >= 0 ? fullsize : 0) +
						(mediumsize >= 0 ? mediumsize : 0) +
						(thumbsize >= 0 ? thumbsize : 0);
					if (total > 0)
						this.DeleteImageResource(resource);
					size -= total;
				}

				if (size > CacheSizeLimit)
				{
					TransactionLog.Instance.Add("Remote collection cache too small",
						String.Format("Collection: {0}\nCurrent size: {1}\nLimit: {2}",
						this.id,
						size,
						CacheSizeLimit));
				}
			}
		}

		/// <summary>
		/// Calculate size of directory
		/// </summary>
		/// <remarks>
		/// Recursively determines the cumulative size of all files in a given
		/// directory and all subdirectories
		/// </remarks>
		/// <param name="dir">The target directory</param>
		/// <returns>The size of all files in bytes</returns>
		protected long GetDirectorySize(DirectoryInfo dir)
		{
			long size = 0;
			if (dir.Exists)
			{
				foreach (DirectoryInfo subdir in dir.GetDirectories())
					size += GetDirectorySize(subdir);
				foreach (FileInfo file in dir.GetFiles())
					size += file.Length;
			}
			return size;
		}

		/// <summary>
		/// Collection Type
		/// </summary>
		/// <remarks>
		/// This property is read-only.
		/// </remarks>
		/// <value>
		/// Always <see cref="CollectionType.Remote"/>.
		/// </value>
		public override CollectionType Type
		{
			get
			{
				return CollectionType.Remote;
			}
		}

		/// <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 override bool IsKeywordSearchable
		{
			get
			{
				// FogBugz case 547: check if remote collection is keyword searchable
				return true;
			}
		}

		/// <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 remote collections, this property is <c>false</c>.
		/// </remarks>
		/// <value><c>false</c></value>
		public override bool HasRecordHistory
		{
			get
			{
				return false;
			}
		}

		/// <summary>
		/// Read only flag
		/// </summary>
		/// <remarks>
		/// Remote collections are always read-only.
		/// </remarks>
		/// <value><c>true</c></value>
		public override bool IsReadOnly
		{
			get
			{
				return true;
			}
		}

		/// <summary>
		/// Query distinct field values - not supported
		/// </summary>
		/// <remarks>
		/// This method is not supported for this type of collection
		/// </remarks>
		/// <param name="field">n/a</param>
		/// <param name="ascending">n/a</param>
		/// <param name="count">n/a</param>
		/// <param name="include">n/a</param>
		/// <param name="startat">n/a</param>
		/// <returns>raises exception</returns>
		/// <exception cref="CoreException">Thrown when this method is called; this
		/// type of collection does not support browsing</exception>
		public override string[] GetBrowseValues(Field field, string startat, bool include, bool ascending, int count)
		{
			throw new CoreException("Remote collections not browseable");
		}


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

		internal override DateTime GetResourceModificationDate(bool enforcepriv, string resource, ImageSize format)
		{
			CacheImageFile(resource, format);
			return base.GetResourceModificationDate(enforcepriv, resource, format);
		}

		internal override long GetResourceSize(bool enforcepriv, string resource, ImageSize format)
		{
			CacheImageFile(resource, format);
			return base.GetResourceSize(enforcepriv, resource, format);
		}

		internal override System.IO.Stream GetResourceData(bool enforcepriv, string resource, ImageSize format)
		{
			CacheImageFile(resource, format);
			return base.GetResourceData(enforcepriv, resource, format);
		}

		/// <summary>
		/// Cache image file in background
		/// </summary>
		/// <remarks>
		/// This is a utility method that gets called from a background task.  It forwards the
		/// cache request to the <see cref="CacheImageFile"/> method.
		/// </remarks>
		/// <param name="task">The background task from which the request originated</param>
		/// <param name="info">Additional background task information</param>
		/// <returns><c>true</c></returns>
		protected bool BackgroundCacheImageFile(BackgroundTask task, object info)
		{
			CacheImageFile((string)task.Data["resource"], (ImageSize)task.Data["format"]);
			return true;
		}

		/// <summary>
		/// Create task title
		/// </summary>
		/// <remarks>
		/// Creates a unique task title containing the collection identifier, image resource
		/// and image size
		/// </remarks>
		/// <param name="resource">The image resource</param>
		/// <param name="size">The image size</param>
		/// <returns>A string containing a task title</returns>
		protected string CreateTaskTitle(string resource, ImageSize size)
		{
			return String.Format("Collection {0} pre-cache {1} {2}",
				this.id,
				resource,
				size.ToString());
		}

		/// <summary>
		/// Cache image file
		/// </summary>
		/// <remarks>
		/// Download and cache an image resource.
		/// </remarks>
		/// <param name="resource">The image resource to cache</param>
		/// <param name="format">The size of the image to cache</param>
		protected virtual void CacheImageFile(string resource, ImageSize format)
		{
			string file = this.MapResource(resource, format);
			if (File.Exists(file))
				return;
			string size;
			if (format == ImageSize.Full)
				size = "F";
			else if (format == ImageSize.Medium)
				size = "M";
			else if (format == ImageSize.Thumbnail)
				size = "T";
			else
				return;
			try
			{
				string remoteid;
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"SELECT RemoteID FROM Images WHERE CollectionID={id} AND Resource={resource}");
					query.AddParam("id", this.id);
					query.AddParam("resource", resource);
					remoteid = conn.DataToString(conn.ExecScalar(query));
				}
				ImageIdentifier imageid = ImageIdentifier.Parse(remoteid);
				ServiceInfo info = (ServiceInfo)GetServiceInfo();
				string getimageurl = info.remoteurl + 
					String.Format("getimage.aspx?id={0}&cid={1}&format={2}",
					imageid.ID, imageid.CollectionID, size);
				HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(getimageurl);
				req.CookieContainer = info.service.CookieContainer;
				Stream stream = req.GetResponse().GetResponseStream();
				this.SetRawResourceData(false, resource, format, stream);
				stream.Close();
			}
			catch (Exception ex)
			{
				TransactionLog.Instance.AddException("Remote image retrieval failed",
					String.Format("CollectionID={0}", this.id),
					ex);
			}
		}

		/// <summary>
		/// Precache image file
		/// </summary>
		/// <remarks>
		/// This method schedules a background task to cache the specified image file
		/// </remarks>
		/// <param name="resource">The image resource to cache</param>
		/// <param name="format">The size of the image to cache</param>
		/// <param name="remoteserver">The remote server the image will be downloaded from</param>
		protected void PrecacheImageFile(string resource, ImageSize format, string remoteserver)
		{
			string mf = this.MapResource(resource, format);
			if (!File.Exists(mf))
			{
				BackgroundTask task = 
					new BackgroundTask(CreateTaskTitle(resource, format),
					0, "remotecollection_" + remoteserver);
				task.OnRun += new BackgroundTaskDelegate(BackgroundCacheImageFile);
				task.Data["resource"] = resource;
				task.Data["format"] = format;
				task.Queue();
			}
		}

		/// <summary>
		/// Precache image
		/// </summary>
		/// <remarks>
		/// Caches an image file before it actually has been requested for display, to significantly
		/// speed up the display lateron.
		/// </remarks>
		/// <param name="image">The image to precache</param>
		public virtual void PrecacheImage(Image image)
		{
			PrecacheImageFile(image.Resource, ImageSize.Medium, RemoteServer);
			PrecacheImageFile(image.Resource, ImageSize.Full, RemoteServer);
		}
	}
}
