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

namespace Orciid.Core
{
	/// <summary>
	/// Data import
	/// </summary>
	/// <remarks>
	/// This class holds functionality to map fields between two collections
	/// or a DataTable and a collection
	/// </remarks>
	[Serializable]
	public class FieldMapping
	{

		// holds all source fields as string keys and mapped fields as string values
		private SortedList mapping = new SortedList();
		// holds all possible target fields (as strings)
		private ArrayList targets = new ArrayList();
		// holds identifier of target collection for mapping persistence
		private int collid = 0;
		// holds additional settings as needed
		private Hashtable settings = new Hashtable();

		private static string[] GetColumnNames(DataTable table)
		{
			ArrayList list = new ArrayList();
			foreach (DataColumn c in table.Columns)
				list.Add(c.Caption);
			return (string[])list.ToArray(typeof(string));
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Used to map fields in a DataTable to fields in a collection
		/// </remarks>
		/// <param name="source">The DataTable holding the source fields</param>
		/// <param name="target">The target collection</param>
		public FieldMapping(DataTable source, Collection target):
			this(GetColumnNames(source), target.GetFields())
		{
			collid = target.ID;
			Recall();
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Used to map fields from a source collection to fields in a target collection
		/// </remarks>
		/// <param name="source">The source collection</param>
		/// <param name="target">The target collection</param>
		public FieldMapping(Collection source, Collection target):
			this(source.GetFields(), target.GetFields())
		{
			collid = target.ID;
			Recall();
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Used to map fields from a list of source field names to a given list of target fields
		/// </remarks>
		/// <param name="source">The list of source field names</param>
		/// <param name="target">The list of target fields</param>
		public FieldMapping(string[] source, Field[] target)
		{
			foreach (string s in source)
				if (!mapping.ContainsKey(s))
					mapping.Add(s, null);
			CreateTargetList(target);
			AutoMapFields(target);
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Used to map fields from a list of source fields to a given list of target fields
		/// </remarks>
		/// <param name="source">The list of source fields</param>
		/// <param name="target">The list of target fields</param>
		public FieldMapping(Field[] source, Field[] target)
		{
			foreach (Field f in source)
				if (!mapping.ContainsKey(f.Name))
					mapping.Add(f.Name, null);
			CreateTargetList(target);
			AutoMapFields(source, target);
		}

		private void CreateTargetList(Field[] target)
		{
			foreach (Field f in target)
			{
				targets.Add(f.Name);
				if (f.Type == FieldType.Date)
				{
					targets.Add(f.Name + "::internal");
					targets.Add(f.Name + "::start");
					targets.Add(f.Name + "::end");
				}
			}
		}

		// automatically maps fields
		private void AutoMapFields(Field[] targetfields)
		{
			ArrayList keys = new ArrayList(mapping.Keys);

			// iterate through fields and map matching source field names 
			// to target Dublin core information
			foreach (string source in keys)
				if (mapping[source] == null)
					foreach (Field target in targetfields)
						if (String.Compare(source, target.DCName, true) == 0)
						{
							mapping[source] = target.Name;
							break;
						}
			
			foreach (string source in keys)
				if (mapping[source] == null)
					foreach (Field target in targetfields)
						if (String.Compare(source, target.DCElement, true) == 0)
						{
							mapping[source] = target.Name;
							break;
						}

			// iterate through fields and map matching source field names 
			// to target field names
			foreach (string source in keys)
				if (mapping[source] == null)
					foreach (Field target in targetfields)
						if (String.Compare(source, target.Name, true) == 0)
						{
							mapping[source] = target.Name;
							break;
						}

			// iterate through fields and map matching source field names 
			// to target field names using SoundEx comparison
			foreach (string source in keys)
				if (mapping[source] == null)
					foreach (Field target in targetfields)
						if (SoundEx.Compare(source, target.Name) == 0)
						{
							mapping[source] = target.Name;
							break;
						}
		}

		// automatically maps fields, using extra field information like Dublin Core
		private void AutoMapFields(Field[] sourcefields, Field[] targetfields)
		{
			// iterate through fields and map matching Dublin Core information
			foreach (Field source in sourcefields)
				if (mapping.ContainsKey(source.Name) && mapping[source.Name] == null)
					foreach (Field target in targetfields)
						if (String.Compare(source.DCName, target.DCName, true) == 0)
						{
							mapping[source.Name] = target.Name;
							break;
						}

			// now do regular automatic mapping on any leftover fields
			AutoMapFields(targetfields);
		}

		/// <summary>
		/// Maps a field
		/// </summary>
		/// <remarks>
		/// This method returns the name of the target field for the specified source field.
		/// </remarks>
		/// <param name="source">The field to be mapped</param>
		/// <returns>The name of the target field, or <c>null</c> if no field is mapped
		/// to the source field</returns>
		public string GetMapping(string source)
		{
			return (string)mapping[source];
		}

        /// <summary>
        /// Get mapped field
        /// </summary>
        /// <remarks>
        /// Returns the target field mapped to a given source field. If the target is
        /// a multi-property field, e.g. a date field, only the field name is returned,
        /// not the property part.
        /// </remarks>
        /// <param name="source">The source field</param>
        /// <returns>The target field, or <c>null</c> if no mapping is found</returns>
		public string GetMappedField(string source)
		{
			string t = GetMapping(source);
			if (t == null)
				return null;
			int p = t.IndexOf("::");
			if (p >= 0)
				return t.Substring(0, p);
			else
				return t;
		}

		/// <summary>
		/// Manual map fields
		/// </summary>
		/// <remarks>
		/// This method allows manually adding a field mapping or overriding an automatic
		/// mapping.
		/// </remarks>
		/// <param name="source">The name of the source field</param>
		/// <param name="target">The name of the target field. Can be <c>null</c>
		/// or an empty string to remove an existing mapping.</param>
		public void AddMapping(string source, string target)
		{
			if (!mapping.ContainsKey(source))
				return;
			if (target != null && target.Length == 0)
				mapping[source] = null;
			else
				mapping[source] = target;
		}

        /// <summary>
        /// Source fields
        /// </summary>
        /// <remarks>
        /// The list of available source fields
        /// </remarks>
        /// <value>
        /// The list of available source fields
        /// </value>
		public string[] SourceFields
		{
			get
			{
				return (string[])(new ArrayList(mapping.Keys)).ToArray(typeof(string));
			}
		}

		/// <summary>
        /// Target fields
        /// </summary>
        /// <remarks>
        /// The list of available target fields
        /// </remarks>
        /// <value>
        /// The list of available target fields
        /// </value>
        public string[] TargetFields
		{
			get
			{
				return (string[])(targets.ToArray(typeof(string)));
			}
		}

        /// <summary>
        /// Store mappings and settings
        /// </summary>
        /// <remarks>
        /// This method stores the current mappings and settings, so they can be
        /// automatically recalled at a later time
        /// </remarks>
		public void Remember()
		{
			Collection coll = (collid == 0 ? null : Collection.GetByID(collid));
			if (coll == null)
				return;
			Properties props = Properties.GetProperties(coll);
			string key = "map" + GetSourceFieldHash(); // key must be 16 or fewer characters, int.ToString() is 11 at most
			StringWriter val = new StringWriter();
			foreach (string s in mapping.Keys)
			{
				val.Write(s);
				val.Write('\t');
				val.Write((string)mapping[s]);
				val.Write('\n');
			}
			foreach (string s in settings.Keys)
			{
				val.Write("***");
				val.Write(s);
				val.Write('\t');
				val.Write((string)settings[s]);
				val.Write('\n');
			}
			props.Set(key, val.ToString());
		}

		private void Recall()
		{
			Collection coll = (collid == 0 ? null : Collection.GetByID(collid));
			if (coll == null)
				return;
			Properties props = Properties.GetProperties(coll);
			string key = "map" + GetSourceFieldHash(); // key must be 16 or fewer characters, int.ToString() is 11 at most
			// load all stored mappings that don't match this source field list
			foreach (string prop in props.Keys)
				if (prop.StartsWith("map") && prop != key)
					RecallSingleMapping(props.Get(prop, null));
			// load stored mappings that match this source field list
			RecallSingleMapping(props.Get(key, null));			
		}

		private void RecallSingleMapping(string val)
		{
			if (val == null)
				return;
			foreach (string keyvalpair in val.Split('\n'))
				if (keyvalpair.IndexOf('\t') > 0)
				{
					string[] s = keyvalpair.Split('\t');
					if (s[0].StartsWith("***"))
						StoreSetting(s[0].Substring(3), s[1]);
					else
						AddMapping(s[0], s[1]);
				}
		}

		private string GetSourceFieldHash()
		{
			StringBuilder builder = new StringBuilder();
			foreach (string s in mapping.Keys)
				builder.Append(s).Append(' ');
			return builder.ToString().GetHashCode().ToString();
		}

        /// <summary>
        /// Store setting
        /// </summary>
        /// <remarks>
        /// This method stores an arbitrary key/value pair for reuse when a
        /// field mapping is repeated in the future.  Used in the import process
        /// to store e.g. the mapping settings for the resource and identifier fields.
        /// </remarks>
        /// <param name="key">The key to store</param>
        /// <param name="val">The value to store</param>
		public void StoreSetting(string key, string val)
		{
			settings[key] = val;
		}

        /// <summary>
        /// Retrieve setting
        /// </summary>
        /// <remarks>
        /// This method retrieves a value based on a key/value pair that was
        /// stored using <see cref="StoreSetting"/>.
        /// </remarks>
        /// <param name="key">The key to look up</param>
        /// <returns>The value associated with the key, or <c>null</c> if no such key</returns>
		public string GetSetting(string key)
		{
			return settings[key] as string;
		}
	}
}
