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;

namespace Orciid.Core
{
	/// <summary>
	/// Data exchange
	/// </summary>
	/// <remarks>
	/// This class contains methods for importing and exporting data
	/// and XML data file validation.
	/// </remarks>
	public class DataExchange
	{
		/// <summary>
		/// XML Schema for a collection
		/// </summary>
		/// <remarks>
		/// This method creates an <see cref="XmlSchema"/> object for a given
		/// collection.  XML files that validate against the create schema can
		/// be imported using the <see cref="Import"/> method.
		/// </remarks>
		/// <param name="coll">The collection to create the schema for</param>
		/// <returns>The XmlSchema for the given collection</returns>
		public static XmlSchema GetXmlSchema(Collection coll)
		{
			XmlQualifiedName stringType = new XmlQualifiedName("string", 
				"http://www.w3.org/2001/XMLSchema");

			XmlSchema schema = new XmlSchema();
			XmlSchemaElement root = new XmlSchemaElement();
			root.Name = "data";
			schema.Items.Add(root);

			XmlSchemaComplexType complextype = new XmlSchemaComplexType();
			root.SchemaType = complextype;

			XmlSchemaAttribute attribute = new XmlSchemaAttribute();
			attribute.Name = "identifier";
			attribute.SchemaTypeName = new XmlQualifiedName("identifierType");
			attribute.Use = XmlSchemaUse.Required;
			complextype.Attributes.Add(attribute);

			XmlSchemaSequence sequence = new XmlSchemaSequence();
			sequence.MaxOccursString = "unbounded";
			complextype.Particle = sequence;

			XmlSchemaElement record = new XmlSchemaElement();
			record.Name = "record";
			sequence.Items.Add(record);

			complextype = new XmlSchemaComplexType();
			record.SchemaType = complextype;

			attribute = new XmlSchemaAttribute();
			attribute.Name = "resource";
			attribute.SchemaTypeName = stringType;
			attribute.Use = XmlSchemaUse.Required;
			complextype.Attributes.Add(attribute);

			XmlSchemaChoice choice = new XmlSchemaChoice();
			choice.MaxOccursString = "unbounded";
			complextype.Particle = choice;

			foreach (Field field in coll.GetFields())
			{
				XmlSchemaElement element = new XmlSchemaElement();
				element.Name = field.Name; // FogBugz case 541: dublin core names
				choice.Items.Add(element);
				if (field.Type == FieldType.Date)
					element.SchemaTypeName = new XmlQualifiedName("dateType");
				else
					element.SchemaTypeName = stringType;
			}

			XmlSchemaSimpleType formatType = new XmlSchemaSimpleType();
			formatType.Name = "formatType";
			schema.Items.Add(formatType);

			XmlSchemaSimpleTypeRestriction formatRestriction = new XmlSchemaSimpleTypeRestriction();
			formatRestriction.BaseTypeName = stringType;
			formatType.Content = formatRestriction;

			foreach (DateInterpreter.Format format in DateInterpreter.Formats)
			{
				XmlSchemaEnumerationFacet enumFacet = new XmlSchemaEnumerationFacet();
				enumFacet.Value = format.ToString();
				formatRestriction.Facets.Add(enumFacet);
			}

			XmlSchemaSimpleType identifierType = new XmlSchemaSimpleType();
			identifierType.Name = "identifierType";
			schema.Items.Add(identifierType);

			XmlSchemaSimpleTypeRestriction identifierRestriction = new XmlSchemaSimpleTypeRestriction();
			identifierRestriction.BaseTypeName = stringType;
			identifierType.Content = identifierRestriction;

			foreach (Field field in coll.GetFields())
			{
				XmlSchemaEnumerationFacet enumFacet = new XmlSchemaEnumerationFacet();
				enumFacet.Value = field.Name;
				identifierRestriction.Facets.Add(enumFacet);
			}

			XmlSchemaComplexType dateType = new XmlSchemaComplexType();
			dateType.Name = "dateType";
			schema.Items.Add(dateType);

			XmlSchemaSimpleContent simplecontent = new XmlSchemaSimpleContent();
			dateType.ContentModel = simplecontent;

			XmlSchemaSimpleContentExtension extension = new XmlSchemaSimpleContentExtension();
			extension.BaseTypeName = stringType;
			simplecontent.Content = extension;

			attribute = new XmlSchemaAttribute();
			attribute.Name = "date";
			attribute.SchemaTypeName = stringType;
			attribute.Use = XmlSchemaUse.Optional;
			extension.Attributes.Add(attribute);

			attribute = new XmlSchemaAttribute();
			attribute.Name = "format";
			attribute.SchemaTypeName = new XmlQualifiedName("formatType");
			attribute.Use = XmlSchemaUse.Optional;
			extension.Attributes.Add(attribute);

			return schema;
		}

		private const int MAX_IMPORT_VALIDATION_ERRORS = 20;

		private class ImportValidationErrorHandler
		{
			public ArrayList errors = new ArrayList();

			public void AddError(string e)
			{
				errors.Add(e);
				if (errors.Count > MAX_IMPORT_VALIDATION_ERRORS)
					throw new ImportValidationException(
						"Too many validation errors:\n" + ErrorMessages);
			}

			public void ErrorHandler(object o, ValidationEventArgs e)
			{
				AddError(e.Message);
			}

			public string ErrorMessages
			{
				get
				{
					return String.Join("\n", (string[])errors.ToArray(typeof(string)));
				}
			}

			public void CheckForErrors()
			{
				if (errors.Count > 0)
					throw new ImportValidationException(
						"Validation errors occurred:\n" + ErrorMessages);
			}
		}

		/// <summary>
		/// Extract field definitions from schema file
		/// </summary>
		/// <remarks>
		/// This method provides a quick way to come up with a set of field definitions
		/// that will satisfy the requirements of a given schema.  Any user interface settings
		/// for the fields are set to default values, since the schema does not contain
		/// any information regarding these settings.
		/// </remarks>
		/// <param name="xmlreader">The XML reader containing the schema</param>
		/// <returns>An array of <see cref="Field"/> objects.</returns>
		public static Field[] GetFieldsFromSchema(XmlReader xmlreader)
		{
			XmlDocument doc = new XmlDocument();
			try
			{
				doc.Load(xmlreader);
			}
			catch
			{
				return null;
			}
			XmlNamespaceManager nsm = new XmlNamespaceManager(doc.NameTable);
			nsm.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema");
			ArrayList fields = new ArrayList();
			XmlNode datanode = doc.SelectSingleNode("/xs:schema/xs:element", nsm);
			if (datanode == null || datanode.Attributes["name"] == null || 
				datanode.Attributes["name"].Value != "data")
				return null;
			XmlNode recordnode = datanode.SelectSingleNode("xs:complexType/xs:sequence/xs:element", nsm);
			if (recordnode == null || recordnode.Attributes["name"] == null ||
				recordnode.Attributes["name"].Value != "record")
				return null;
			XmlNodeList elements = recordnode.SelectNodes("xs:complexType/xs:choice/xs:element", nsm);
			if (elements == null)
				return null;
			foreach (XmlNode node in elements)
			{
				XmlAttribute typeattr = node.Attributes["type"];
				XmlAttribute nameattr = node.Attributes["name"];
				string type = (typeattr != null ? typeattr.Value : null);
				string name = (nameattr != null ? nameattr.Value : null);
				if (type == null || type.Length == 0 || name == null || name.Length == 0)
					return null;
				Field f = new Field();
				f.KeywordSearchable = true;
				f.Sortable = true;
				f.Searchable = true;
				f.Browsable = true;
				f.ShortView = DisplayMode.All;
				f.MediumView = DisplayMode.All;
				f.LongView = DisplayMode.All;
				f.Name = name;
				f.Label = name;
				f.Type = (type == "dateType" ? FieldType.Date : FieldType.Text);
				fields.Add(f);
			}
			return (Field[])fields.ToArray(typeof(Field));
		}

		/// <summary>
		/// Extract field definitions from data file
		/// </summary>
		/// <remarks>
		/// This method provides a quick way to come up with a set of field definitions
		/// that will satisfy the requirements of a given data file.  Any user interface settings
		/// for the fields are set to default values, since the data file does not contain
		/// any information regarding these settings.
		/// </remarks>
		/// <param name="xmlreader">The XML reader containing the data</param>
		/// <returns>An array of <see cref="Field"/> objects.</returns>
		public static Field[] GetFieldsFromData(XmlReader xmlreader)
		{
			XmlDocument doc = new XmlDocument();
			try
			{
				doc.Load(xmlreader);
			}
			catch (Exception ex)
			{
				TransactionLog.Instance.AddException("GetFieldsFromData", 
					"Could not read fields from XmlReader", ex);
				return null;
			}
			Hashtable fields = new Hashtable();
			XmlNodeList elements = doc.SelectNodes("/data/record/*");
			if (elements == null)
				return null;
			foreach (XmlNode node in elements)
			{
				bool datefield = (node.Attributes["date"] != null || 
					node.Attributes["format"] != null);
				if (fields.ContainsKey(node.Name))
				{
					if (datefield)
						((Field)fields[node.Name]).Type = FieldType.Date;
				}
				else
				{
					Field f = new Field();
					f.KeywordSearchable = true;
					f.Sortable = true;
					f.Searchable = true;
					f.Browsable = true;
					f.ShortView = DisplayMode.All;
					f.MediumView = DisplayMode.All;
					f.LongView = DisplayMode.All;
					f.Name = node.Name;
					f.Label = node.Name;
					f.Type = (datefield ? FieldType.Date : FieldType.Text);
					fields.Add(node.Name, f);
				}
			}
			Field[] rf = new Field[fields.Count];
			fields.Values.CopyTo(rf, 0);
			return rf;
		}

		/// <summary>
		/// Export collection data
		/// </summary>
		/// <remarks>
		/// Creates an XML document with all image record data.  The document could it turn
		/// be used with the <see cref="Import"/> method.
		/// </remarks>
		/// <param name="coll">The collection to export</param>
		/// <param name="idfield">The field to be used as record identifier</param>
		/// <param name="includepersonal">Determines if personal images should be exported as well</param>
		/// <param name="textwriter">The stream to write the XML document to</param>
		/// <returns>An XML document containing all image records in this collection</returns>
		public void ExportToStream(TextWriter textwriter, Collection coll, 
			Field idfield, bool includepersonal)
		{
			if (coll == null)
				throw new ArgumentException("Collection must not be null.");
			if (idfield == null)
				throw new ArgumentException("Identifier field must not be null.");
			if (idfield.CollectionID != coll.ID)
				throw new ArgumentException("Identifier field is not in specified collection.");
			XmlTextWriter writer = new XmlTextWriter(textwriter);
			XmlDocument doc = new XmlDocument();
			writer.Indentation = 1;
			writer.IndentChar = '\t';
			writer.Formatting = Formatting.Indented;
			writer.WriteStartDocument(true);
			writer.WriteStartElement("data");
			writer.WriteAttributeString("identifier", idfield.Name);			
			int[] ids;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT ID FROM Images WHERE CollectionID={collectionid}" +
					(includepersonal ? "" : @" AND (UserID IS NULL OR UserID=0)"));
				query.AddParam("collectionid", coll.ID);
				ids = conn.TableToArray(conn.SelectQuery(query));
			}
			int current = 0;
			while (current < ids.Length)
			{
				int length = Math.Min(ids.Length - current, 100);
				ImageIdentifier[] imageids = new ImageIdentifier[length];
				for (int i = 0; i < length; i++)
					imageids[i] = new ImageIdentifier(ids[current + i], coll.ID);
				Image[] images = Image.GetByID(imageids);
				foreach (Image i in images)
					i.GetDataAsXml(doc).WriteTo(writer);
				current += length;
			}
			writer.WriteEndElement();
			writer.WriteEndDocument();
			writer.Flush();
		}
	}

	/// <summary>
	/// The record processing result
	/// </summary>
	/// <remarks>
	/// Every time a record is processed, an event handler is called.  This enumeration holds
	/// the values for the result event argument.
	/// </remarks>
	public enum ImportRecordResult
	{
		/// <summary>
		/// Record already exists
		/// </summary>
		/// <remarks>
		/// The record could not be processed because another record with the same identifier
		/// already exists and flag allowing record replacements was not set
		/// </remarks>
		FailedExisted,
		/// <summary>
		/// Record replaced
		/// </summary>
		/// <remarks>
		/// The record was processed successfully and replaced an existing record.
		/// </remarks>
		Replaced,
		/// <summary>
		/// Record added
		/// </summary>
		/// <remarks>
		/// The record was processed successfully and added to the database.
		/// </remarks>
		Added
	}
}
