using System;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Collections;
using System.Data;
using System.Security.Principal;
using System.Runtime.Remoting.Messaging;

namespace Orciid.Core
{
	/// <summary>
	/// Data import
	/// </summary>
	/// <remarks>
	/// This class holds functionality to import catalog data files
	/// </remarks>
	public class Import
	{
        /// <summary>
        /// Import status
        /// </summary>
        /// <remarks>
        /// The status of an ongoing or past data import
        /// </remarks>
		public class ImportStatus
		{
			private int read;
			private int added;
			private int replaced;
			private int skipped;
			private string errors = "";
			private ArrayList imageIdentifiers = new ArrayList();

            /// <summary>
            /// Read records
            /// </summary>
            /// <remarks>
            /// Returns the number of records that were read during the import
            /// </remarks>
			public int Read
			{
				get
				{
					return read;
				}
			}

			internal void IncrementRead()
			{
				read++;
			}

            /// <summary>
            /// Added records
            /// </summary>
            /// <remarks>
            /// Returns the number of records that were added during the import
            /// </remarks>
			public int Added
			{
				get
				{
					return added;
				}
			}

			internal void IncrementAdded()
			{
				added++;
			}

            /// <summary>
            /// Replaced records
            /// </summary>
            /// <remarks>
            /// Returns the number of records that were replaced during the import
            /// </remarks>
            public int Replaced
			{
				get
				{
					return replaced;
				}
			}

			internal void IncrementReplaced()
			{
				replaced++;
			}

            /// <summary>
            /// Skipped records
            /// </summary>
            /// <remarks>
            /// Returns the number of records that were skipped during the import
            /// </remarks>
			public int Skipped
			{
				get
				{
					return skipped;
				}
			}

			internal void IncrementSkipped()
			{
				skipped++;
			}

            /// <summary>
            /// Errors
            /// </summary>
            /// <remarks>
            /// Returns the number of errors that occurred during the import
            /// </remarks>
			public string Errors
			{
				get
				{
					return errors;
				}
			}

            /// <summary>
            /// Image identifiers
            /// </summary>
            /// <remarks>
            /// A list of the image identifiers of the imported records
            /// </remarks>
			public ArrayList ImageIdentifiers
			{
				get
				{
					return (ArrayList)imageIdentifiers.Clone();
				}
			}

			internal void AddImageIdentifier(ImageIdentifier id)
			{
				imageIdentifiers.Add(id);
			}

            /// <summary>
            /// Add error
            /// </summary>
            /// <remarks>
            /// Add an error message to the import status log
            /// </remarks>
            /// <param name="s">The error message to add</param>
			public void AddError(string s)
			{
				errors += String.Format("Record {0}: {1}\n", Read, s);
			}
		}

		/// <summary>
		/// Import Record Event Arguments
		/// </summary>
		/// <remarks>
		/// An instance of this classed is passed to the <see cref="Import.OnImportRecord"/>
		/// event handler.  It holds information about the processed record, the result, and a
		/// return flag that can be used to abort the import process.
		/// </remarks>
		public class ImportEventArgs:
			EventArgs
		{
            /// <summary>
            /// Import Status
            /// </summary>
            /// <remarks>
            /// The status of the ongoing import
            /// </remarks>
			public readonly ImportStatus Status;

			private bool abort = false;

			/// <summary>
			/// Abortion flag
			/// </summary>
			/// <remarks>
			/// This flag can be set by the event handler.  Once it is set to <c>true</c>, no further
			/// records will be processed by <see cref="Import"/>.
			/// </remarks>
			/// <value>
			/// <c>true</c> if the import process should be aborted after this record, <c>false</c>
			/// otherwise.
			/// </value>
			public bool Abort
			{
				set
				{
					if (value && !abort)
						abort = true;
				}
				get
				{
					return abort;
				}
			}

			/// <summary>
			/// Constructor
			/// </summary>
			/// <remarks>
			/// Default constructor
			/// </remarks>
			/// <param name="status">The current status of the record processing</param>
			public ImportEventArgs(ImportStatus status):
				base()
			{
				Status = status;
			}
		}

		private Collection collection;
		private DataTable data;
		private FieldMapping mapping;
		private bool replace;
		private bool testrun;
		private string idfield;
		private string resourcefield;
		private DateInterpreter.Format dateformat;

		// list of duplicate image identifiers currently in collection
		private ArrayList duplicateids;
		// list of image identifiers with corresponding internal identifier
		private Hashtable existingids;

        /// <summary>
        /// Event handler
        /// </summary>
        /// <remarks>
        /// called with a status report on current import after each record
        /// </remarks>
		public event EventHandler OnImportRecord;

		/// <summary>
		/// Set up a new data import
		/// </summary>
		/// <remarks>
		/// For each data import, create a new instance of this class
		/// </remarks>
		/// <param name="coll">The target collection that will hold
		/// the imported records</param>
		/// <param name="data">The data to import</param>
		/// <param name="dateformat">The format date information is stored in in the source data</param>
		/// <param name="idfield">The field that contains the image identifiers</param>
		/// <param name="mapping">The field mapping to use during the import</param>
		/// <param name="replace">If <c>false</c>, no records will be overwritten</param>
		/// <param name="resourcefield">The field that contains the image resources</param>
		/// <param name="testrun">If <c>true</c>, records will not actually be added or updated</param>
		public Import(Collection coll, DataTable data, FieldMapping mapping, 
			string idfield, string resourcefield, bool replace, bool testrun,
			DateInterpreter.Format dateformat)
		{
			this.collection = coll;
			this.data = data;
			this.mapping = mapping;
			this.replace = replace;
			this.testrun = testrun;
			this.idfield = idfield;
			this.resourcefield = resourcefield;
			this.dateformat = dateformat;

			if (!data.Columns.Contains(idfield))
				throw new CoreException("Identifier field not found in source data");
			if (!data.Columns.Contains(resourcefield))
				throw new CoreException("Resource field not found in source data");
			if (mapping.GetMapping(idfield) == null)
				throw new CoreException("Identifier field not mapped to target field");
		}

		private void CollectIdentifiers()
		{
			duplicateids = new ArrayList();
			existingids = new Hashtable(); // identifier => image.id.id mapping			
			using (DBConnection conn = DBConnector.GetConnection())
			{	
				Field idf = collection.GetField(mapping.GetMapping(idfield));
				Query query = new Query(conn,
					@"SELECT ImageID,FieldValue FROM FieldData INNER JOIN Images ON
					(ImageID=Images.ID AND FieldData.CollectionID=Images.CollectionID)
					WHERE FieldData.CollectionID={collectionid} AND FieldID={fieldid}
					AND (UserID IS NULL OR UserID=0)");
				query.AddParam("collectionid", collection.ID);
				query.AddParam("fieldid", idf.ID);
				DataTable table = conn.SelectQuery(query);
				foreach (DataRow row in table.Rows)
				{
					string key = conn.DataToString(row["FieldValue"]).ToLower();
					int val = conn.DataToInt(row["ImageID"], 0);
					if (!existingids.ContainsKey(key))
						existingids.Add(key, val);
					else if (!duplicateids.Contains(key))
						duplicateids.Add(key);
				}
			}
		}

		private string ValueOf(object o)
		{
			string s = o as string;
			if (s != null && s.Length == 0)
				s = null;
			return s;
		}

		private bool FireOnImportRecord(ImportStatus status)
		{
			if (OnImportRecord == null)
				return true;

			ImportEventArgs e = new ImportEventArgs(status);
			OnImportRecord(this, e);
			return !e.Abort;
		}

        /// <summary>
        /// Run import
        /// </summary>
        /// <remarks>
        /// This method starts the data import
        /// </remarks>
        /// <returns>The status of the import</returns>
		public ImportStatus Run()
		{
			User.RequirePrivilege(Privilege.ModifyImages, collection);

			CollectIdentifiers();
			ImportStatus status = new ImportStatus();
			Image current = null;

			foreach (DataRow row in data.Rows)
			{
				string id = ValueOf(row[idfield]);

				if (id != null)
				{
					if (current != null)
					{
						// save current image, unless in test run
						if (!testrun)
							current.Update();
						status.AddImageIdentifier(current.ID);
						current = null;
						if (!FireOnImportRecord(status))
							break;
					}
					status.IncrementRead();

					string lowerid = id.ToLower();
					if (existingids.Contains(lowerid))
					{
						// existing image
						if (replace)
						{
							if (duplicateids.Contains(lowerid))
							{
								status.AddError(String.Format(
									"Duplicate identifier '{0}' in target collection, " +
									"cannot replace record",
									id));
								current = null;
								continue;
							}
							current = Image.GetByID(
								new ImageIdentifier((int)existingids[lowerid], collection.ID));
							// remove existing field values
							foreach (Field field in collection.GetFields())
								current[field].RemoveAll();
							status.IncrementReplaced();							
						}
						else
						{
							// skip image
							current = null;
							status.IncrementSkipped();
							continue;
						}
					}
					else
					{
						// new image
						current = collection.CreateImage(false);
						status.IncrementAdded();			
					}
				}

				if (current != null)
				{
					// store image resource
					string resource = ValueOf(row[resourcefield]);
					if (resource != null)
						current.Resource = resource;
					// add data items to image
					// loop through each possible target field
					foreach (Field field in collection.GetFields())
					{
						ArrayList values = null;
						ArrayList datevalues = null;
						ArrayList datestartvalues = null;
						ArrayList dateendvalues = null;
						if (field.Type == FieldType.Date)
						{
							values = new ArrayList();
							datevalues = new ArrayList();
							datestartvalues = new ArrayList();
							dateendvalues = new ArrayList();
						}

						// loop through mappings and find source values
						foreach (string source in mapping.SourceFields)
						{
							if (mapping.GetMappedField(source) == field.Name)
							{
								// get field value
								string v = ValueOf(row[source]);
								if (v == null)
									continue;
								// check for date field special case
								if (field.Type != FieldType.Date)
								{
									current[field].Add(v);
								}
								else
								{
									string target = mapping.GetMapping(source);
									if (target.EndsWith("::internal"))
										datevalues.Add(v);
									else if (target.EndsWith("::start"))
										datestartvalues.Add(v);
									else if (target.EndsWith("::end"))
										dateendvalues.Add(v);
									else
										values.Add(v);
								}
							}
						}
						// store found values in field
						if (field.Type == FieldType.Date)
						{
							// make sure all the lists have the same
							// number of elements
							int count = Math.Max(values.Count, 
								Math.Max(datevalues.Count,
								Math.Max(datestartvalues.Count, dateendvalues.Count)));

							FieldDataDate fielddata = (FieldDataDate)current[field];
							for (int i = 0; i < count; i++)
							{		
								string val = (i < values.Count ? (string)values[i] : null);
								try
								{									
									string orig = (i < datevalues.Count ? (string)datevalues[i] : null);
									int start = (i < datestartvalues.Count ? Int32.Parse((string)datestartvalues[i]) : Int32.MinValue);
									int end = (i < dateendvalues.Count ? Int32.Parse((string)dateendvalues[i]) : Int32.MaxValue);

									if (val == null)
									{
										if (orig == null)
											val = String.Format("{0}-{1}", start, end);
										else
											val = orig;
									}

									if (start != Int32.MinValue || end != Int32.MaxValue)
										fielddata.Add(val, orig, start, end);
									else if (orig == null)
										fielddata.Add(val, val, dateformat);
									else
                                        fielddata.Add(val, orig, dateformat);
								}
								catch (CoreException)
								{
									string message;
									if (val != null)
									{
										current[field].Add(val);
										message = "Could not interpret date value, added display value only";
									}
									else
										message = "Could not interpret date value and no display value available";
									status.AddError(
										String.Format("{0} (Record {1})",
										message, id));
								}
							}
						}
					}
				}
			}

			if (current != null)
			{				
				// save current image, unless in test run
				if (!testrun)
					current.Update();				
				status.AddImageIdentifier(current.ID);
			}

			FireOnImportRecord(status);

			return status;
		}

        /// <summary>
        /// Run import asynchronously
        /// </summary>
        /// <remarks>
        /// Starts the import process on another thread
        /// </remarks>
        /// <param name="callback">The callback for the import process</param>
		public void RunAsync(AsyncCallback callback)
		{
			RunAsyncDelegate runasync = new RunAsyncDelegate(RunAsyncWrapper);
			runasync.BeginInvoke(
				Orciid.Core.User.CurrentUser(), 
				WindowsIdentity.GetCurrent(), 
				callback, 
				null);
		}

        /// <summary>
        /// Get import results
        /// </summary>
        /// <remarks>
        /// Returns the import status for an import that is running on another thread
        /// </remarks>
        /// <param name="iasyncresult"></param>
        /// <returns></returns>
		public static ImportStatus GetAsyncResult(IAsyncResult iasyncresult)
		{
			AsyncResult asyncresult = (AsyncResult)iasyncresult;
			RunAsyncDelegate runasync = (RunAsyncDelegate)asyncresult.AsyncDelegate;
			// this will retrieve the result of the asynchronous call
			// and re-raise any exceptions from the asynchronous thread in this thread
			return runasync.EndInvoke(iasyncresult);
		}

		private delegate ImportStatus RunAsyncDelegate(User user, WindowsIdentity winident);

		private ImportStatus RunAsyncWrapper(User user, WindowsIdentity winident)
		{
			if (winident != null)
				winident.Impersonate();
			if (user != null)
				user.Activate(user.IPAddress);
			return Run();
		}
	}
}
