//
// Mono.Xml.Schema.XsdIdentityState.cs
//
// Author:
//	Atsushi Enomoto (ginga@kit.hi-ho.ne.jp)
//
//	(C)2003 Atsushi Enomoto
//
// These classses represents XML Schema's identity constraints validation state,
// created by xs:key, xs:keyref, xs:unique in xs:element.
//
//
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Xml;
using System.Xml.Schema;

namespace Mono.Xml.Schema
{
	public class XsdKeyEntryField
	{
		XsdKeyEntry entry;
		XsdIdentityField field;

		public XsdKeyEntryField (XsdKeyEntry entry, XsdIdentityField field)
		{
			this.entry = entry;
			this.field = field;
		}

		public XsdIdentityField Field {
			get { return field; }
		}

		public bool FieldFound;
		public int FieldLineNumber;
		public int FieldLinePosition;
		public bool FieldHasLineInfo;
		public XsdAnySimpleType FieldType;

		public object Identity;
		public bool IsXsiNil;

		public int FieldFoundDepth;
		public XsdIdentityPath FieldFoundPath;

		public bool Consuming;
		public bool Consumed;

		// Return false if there is already the same key.
		public bool SetIdentityField (object identity, bool isXsiNil, XsdAnySimpleType type, XsdValidatingReader reader)
		{
			IXmlLineInfo li = reader as IXmlLineInfo;

			FieldFoundDepth = reader.Depth;
			Identity = identity;
			IsXsiNil = isXsiNil;
			FieldFound |= isXsiNil;
			FieldType = type;
			Consuming = false;
			Consumed = true;
			if (li != null && li.HasLineInfo ()) {
				FieldHasLineInfo = true;
				FieldLineNumber = li.LineNumber;
				FieldLinePosition = li.LinePosition;
			}

			if (!(this.entry.KeySequence.SourceSchemaIdentity is XmlSchemaKeyref)) {
				foreach (XsdKeyEntry other in entry.KeySequence.FinishedEntries) {
					// TODO: XmlSchemaDatatypeEquals
					XsdKeyEntryField of = other.KeyFields [field.Index];
					/*
					if (XmlSchemaUtil.IsSchemaDatatypeEquals (
						type, identity, of.FieldType, of.Identity))
						return false;
					*/
					if (this.entry.CompareIdentity (other))
						return false;
				}
			}

			return true;
		}

		// It might throw Exception including other than XmlSchemaException (ReadTypedValue).
		internal XsdIdentityPath FieldMatches (ArrayList qnameStack, XsdValidatingReader reader)
		{
			foreach (XsdIdentityPath path in field.Paths) {
				if (FieldFound && (reader.Depth > this.FieldFoundDepth && this.FieldFoundPath == path))
					continue;

				// Only "." hits.
				if (path.OrderedSteps.Length == 0) {
					if (reader.Depth == entry.StartDepth)
						return path;
					else
						continue;
				}
				// It does not hit as yet (too shallow to hit).
				if (reader.Depth - entry.StartDepth < path.OrderedSteps.Length - 1)
					continue;

				bool isAttributePath = false;
				int iter = path.OrderedSteps.Length;
				if (path.OrderedSteps [iter-1].IsAttribute) {
					isAttributePath = true;
					iter--;
				}
				if (path.Descendants && reader.Depth < entry.StartDepth + iter)
					continue;
				else if (!path.Descendants && reader.Depth != entry.StartDepth + iter)
					continue;

				iter--;

				XsdIdentityStep step;
				for (; iter >= 0; iter--) {
					step = path.OrderedSteps [iter];
					if (step.IsAnyName)
						continue;
					XmlQualifiedName qname = (XmlQualifiedName) qnameStack [entry.StartDepth + iter + 1];
					if (step.NsName != null && qname.Namespace == step.NsName)
						continue;
					if ((step.Name == "*" || step.Name == qname.Name) &&
						step.Namespace == qname.Namespace)
						continue;
					else
						break;
				}
				if (iter >= 0)	// i.e. did not match against the path.
					continue;
				if (!isAttributePath)
					return path;
				step = path.OrderedSteps [path.OrderedSteps.Length - 1];
				if (step.IsAnyName || step.NsName != null) {
					try {
						while (reader.MoveToNextAttribute ()) {
							if (reader.NamespaceURI == XmlSchema.InstanceNamespace)
								continue;
							if (step.IsAnyName || reader.NamespaceURI == step.NsName) {
								this.FillAttributeFieldValue (reader);
							}
						}
					} finally {
						reader.MoveToElement ();
					}
					if (this.Identity != null)
						return path;
					else
						continue;
				}
				if (reader.MoveToAttribute (step.Name, step.Namespace)) {
					this.FillAttributeFieldValue (reader);
					reader.MoveToElement ();
					return path;
				}
				else
					continue;
			}
			return null;
		}

		private void FillAttributeFieldValue (XsdValidatingReader reader)
		{
			if (this.FieldFound)
				throw new XmlSchemaException ("The key value was was already found."
					+ (this.FieldHasLineInfo ?
						String.Format (" At line {0}, position {1}.", FieldLineNumber, FieldLinePosition) :
						""),
					reader, reader.BaseURI, entry.KeySequence.SourceSchemaIdentity, null);
			XmlSchemaDatatype dt = reader.SchemaType as XmlSchemaDatatype;
			XmlSchemaSimpleType st = reader.SchemaType as XmlSchemaSimpleType;
			if (dt == null && st != null)
				dt = st.Datatype;
			object identity = reader.ReadTypedValue ();
			if (identity == null)
				identity = reader.Value;
			if (!this.SetIdentityField (identity, reader.Depth - 1 == reader.XsiNilDepth , dt as XsdAnySimpleType, reader))
				throw new XmlSchemaException ("Two or more identical field was found.", 
					reader, reader.BaseURI, entry.KeySequence.SourceSchemaIdentity, null);
			// HACK: This is not logical. Attributes never be cosuming,
			// so I used it as a temporary mark to sign it is *just* validated now.
			this.Consuming = true;
			this.FieldFound = true;
		}
	}

	public class XsdKeyEntryFieldCollection : IList
	{
		ArrayList al = new ArrayList ();

		#region IList
		public bool IsReadOnly {
			get { return false; }
		}

		object IList.this [int i] {
			get { return al [i]; }
			set { al [i] = value; }
		}

		public XsdKeyEntryField this [int i] {
			get { return (XsdKeyEntryField) al [i]; }
			set { al [i] = value; }
		}

		public void RemoveAt (int i)
		{
			al.RemoveAt (i);
		}

		public void Insert (int i, object value)
		{
			al.Insert (i, value);
		}

		public void Remove (object value)
		{
			al.Remove (value);
		}

		public bool Contains (object value)
		{
			return al.Contains (value);
		}

		public void Clear ()
		{
			al.Clear ();
		}

		public int IndexOf (object value)
		{
			return al.IndexOf (value);
		}

		public int Add (object value)
		{
			return al.Add (value);
		}

		public bool IsFixedSize {
			get { return false; }
		}

		#endregion

		#region ICollection

		public bool IsSynchronized {
			get { return al.IsSynchronized; }
		}

		public int Count {
			get { return al.Count; }
		}

		public void CopyTo(Array array, int i)
		{
			al.CopyTo (array, i);
		}

		public object SyncRoot {
			get { return al.SyncRoot; }
		}

		#endregion

		#region IEnumerable

		public IEnumerator GetEnumerator ()
		{
			return ((IEnumerable) al).GetEnumerator ();
		}

		#endregion

	}

	// Created per field/key pair, created per selector-matched element.
	public class XsdKeyEntry
	{
		public int StartDepth;
		public int CurrentStep;

		public bool ConsumptionTargetIsKey;

		public int SelectorLineNumber;
		public int SelectorLinePosition;
		public bool SelectorHasLineInfo;

		public XsdKeyEntryFieldCollection KeyFields;

		public bool KeyRefFound;
		public int KeyRefSelectorLineNumber;
		public int KeyRefSelectorLinePosition;
		public bool KeyRefSelectorHasLineInfo;

		public XsdKeyTable KeySequence;
		private bool keyFound = false;

		public XsdKeyEntry (XsdKeyTable keyseq, XmlReader reader)
		{
			Init (keyseq, reader);
		}

		public bool KeyFound {
			get {
				if (keyFound)
					return true;
				foreach (XsdKeyEntryField kf in KeyFields) {
					if (kf.FieldFound) {
						keyFound = true;
						return true;
					}
				}
				return false;
			}
		}

		private void Init (XsdKeyTable keyseq, XmlReader reader)
		{
			KeySequence = keyseq;
			KeyFields = new XsdKeyEntryFieldCollection ();
			foreach (XsdIdentityField f in keyseq.Selector.Fields)
				KeyFields.Add (new XsdKeyEntryField (this, f));
			StartDepth = reader.Depth;
			IXmlLineInfo li = reader as IXmlLineInfo;
			if (li != null) {
				if (li.HasLineInfo ()) {
					this.SelectorHasLineInfo = true;
					this.SelectorLineNumber = li.LineNumber;
					this.SelectorLinePosition = li.LinePosition;
				}
			}
		}

		public bool CompareIdentity (XsdKeyEntry other)
		{
			for (int i = 0; i < KeyFields.Count; i++) {
				XsdKeyEntryField f = this.KeyFields [i];
				XsdKeyEntryField of = other.KeyFields [i];
				if (f.IsXsiNil && !of.IsXsiNil || !f.IsXsiNil && of.IsXsiNil)
					return false;
				if (!XmlSchemaUtil.IsSchemaDatatypeEquals (
					of.FieldType, of.Identity, f.FieldType, f.Identity))
					return false;	// does not match
			}
			return true;	// matches
		}

		// In this method, attributes are ignored.
		// It might throw Exception including non-XmlSchemaException.
		public void FieldMatches (ArrayList qnameStack, XsdValidatingReader reader)
		{
			foreach (XsdKeyEntryField keyField in KeyFields) {
				XsdIdentityField fieldDef = keyField.Field;
				XsdIdentityPath path = keyField.FieldMatches (qnameStack, reader);
				if (path != null) {
					if (keyField.FieldFound) {
						// HACK: This is not logical by nature. Attributes never be cosuming,
						// so I used it as a temporary mark to sign it is *just* validated now.
						if (!keyField.Consuming)
							throw new XmlSchemaException ("Two or more matching field was found.",
								reader, reader.BaseURI, this.KeySequence.SourceSchemaIdentity, null);
						else
							keyField.Consuming = false;
					}
					if (!keyField.Consumed) {
						if (reader.XsiNilDepth == reader.Depth && !keyField.SetIdentityField (Guid.Empty, true, XsdAnySimpleType.Instance, reader))
							throw new XmlSchemaException ("Two or more identical field was found.", reader, reader.BaseURI, KeySequence.SourceSchemaIdentity, null);
						else {
							XmlSchemaComplexType ct = reader.SchemaType as XmlSchemaComplexType;
							if (ct != null && 
								(ct.ContentType == XmlSchemaContentType.Empty || ct.ContentType == XmlSchemaContentType.ElementOnly) && 
								reader.SchemaType != XmlSchemaComplexType.AnyType)
								throw new XmlSchemaException ("Specified schema type is complex type, which is not allowed for identity constraints.", reader, reader.BaseURI, KeySequence.SourceSchemaIdentity, null);
							keyField.FieldFound = true;
							keyField.FieldFoundPath = path;
							keyField.FieldFoundDepth = reader.Depth;
							keyField.Consuming = true;
							IXmlLineInfo li = reader as IXmlLineInfo;
							if (li != null && li.HasLineInfo ()) {
								keyField.FieldHasLineInfo = true;
								keyField.FieldLineNumber = li.LineNumber;
								keyField.FieldLinePosition = li.LinePosition;
							}
							reader.CurrentKeyFieldConsumers.Add (keyField);
						}
					}
				}
			}
		}
	}
}
