//
// System.Xml.XmlAttributeCollection
//
// Author:
//   Jason Diamond (jason@injektilo.org)
//   Atsushi Enomoto (ginga@kit.hi-ho.ne.jp)
//
// (C) 2002 Jason Diamond  http://injektilo.org/
// (C) 2002 Atsushi Enomoto
//

//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

using System;
using System.Collections;
using Mono.Xml;

namespace System.Xml
{
#if NET_2_0
	public sealed class XmlAttributeCollection : XmlNamedNodeMap, ICollection
#else
	public class XmlAttributeCollection : XmlNamedNodeMap, ICollection
#endif
	{
		XmlElement ownerElement;
		XmlDocument ownerDocument;

		internal XmlAttributeCollection (XmlNode parent) : base (parent)
		{
			ownerElement = parent as XmlElement;
			ownerDocument = parent.OwnerDocument;
			if(ownerElement == null)
				throw new XmlException ("invalid construction for XmlAttributeCollection.");
		}

		bool ICollection.IsSynchronized {
			get { return false; }
		}

		bool IsReadOnly {
			get { return ownerElement.IsReadOnly; }
		}

		[System.Runtime.CompilerServices.IndexerName ("ItemOf")]
#if NET_2_0
		public XmlAttribute this [string name] {
#else
		public virtual XmlAttribute this [string name] {
#endif
			get {
				return (XmlAttribute) GetNamedItem (name);
			}
		}

		[System.Runtime.CompilerServices.IndexerName ("ItemOf")]
#if NET_2_0
		public XmlAttribute this [int i] {
#else
		public virtual XmlAttribute this [int i] {
#endif
			get {
				return (XmlAttribute) Nodes [i];
			}
		}

		[System.Runtime.CompilerServices.IndexerName ("ItemOf")]
#if NET_2_0
		public XmlAttribute this [string localName, string namespaceURI] {
#else
		public virtual XmlAttribute this [string localName, string namespaceURI] {
#endif
			get {
				return (XmlAttribute) GetNamedItem (localName, namespaceURI);
			}
		}

		object ICollection.SyncRoot {
			get { return this; }
		}

#if NET_2_0
		public XmlAttribute Append (XmlAttribute node) 
#else
		public virtual XmlAttribute Append (XmlAttribute node) 
#endif
		{
			SetNamedItem (node);
			return node;
		}

		public void CopyTo (XmlAttribute[] array, int index)
		{
			// assuming that Nodes is a correct collection.
			for(int i=0; i<Count; i++)
				array [index + i] = Nodes [i] as XmlAttribute;
		}

		void ICollection.CopyTo (Array array, int index)
		{
			// assuming that Nodes is a correct collection.
			array.CopyTo (Nodes.ToArray (typeof(XmlAttribute)), index);
		}

#if NET_2_0
		public XmlAttribute InsertAfter (XmlAttribute newNode, XmlAttribute refNode)
#else
		public virtual XmlAttribute InsertAfter (XmlAttribute newNode, XmlAttribute refNode)
#endif
		{
			if (refNode == null) {
				if (Count == 0)
					return InsertBefore (newNode, null);
				else
					return InsertBefore (newNode, this [0]);
			}
			for (int i = 0; i < Count; i++)
				if (refNode == Nodes [i])
					return InsertBefore (newNode, Count == i + 1 ? null : this [i + 1]);

			throw new ArgumentException ("refNode not found in this collection.");
		}

#if NET_2_0
		public XmlAttribute InsertBefore (XmlAttribute newNode, XmlAttribute refNode)
#else
		public virtual XmlAttribute InsertBefore (XmlAttribute newNode, XmlAttribute refNode)
#endif
		{
			if (newNode.OwnerDocument != ownerDocument)
				throw new ArgumentException ("different document created this newNode.");

			ownerDocument.onNodeInserting (newNode, null);

			int pos = Count;
			if (refNode != null) {
				for (int i = 0; i < Count; i++) {
					XmlNode n = Nodes [i] as XmlNode;
					if (n == refNode) {
						pos = i;
						break;
					}
				}
				if (pos == Count)
					throw new ArgumentException ("refNode not found in this collection.");
			}
			SetNamedItem (newNode, pos, false);

			ownerDocument.onNodeInserted (newNode, null);

			return newNode;
		}

#if NET_2_0
		public XmlAttribute Prepend (XmlAttribute node) 
#else
		public virtual XmlAttribute Prepend (XmlAttribute node) 
#endif
		{
			return this.InsertAfter (node, null);
		}

#if NET_2_0
		public XmlAttribute Remove (XmlAttribute node) 
#else
		public virtual XmlAttribute Remove (XmlAttribute node) 
#endif
		{
			if (IsReadOnly)
				throw new ArgumentException ("This attribute collection is read-only.");
			if (node == null)
				throw new ArgumentException ("Specified node is null.");
			if (node.OwnerDocument != ownerDocument)
				throw new ArgumentException ("Specified node is in a different document.");
			if (node.OwnerElement != this.ownerElement)
				throw new ArgumentException ("The specified attribute is not contained in the element.");

			XmlAttribute retAttr = null;
			for (int i = 0; i < Count; i++) {
				XmlAttribute attr = (XmlAttribute) Nodes [i];
				if (attr == node) {
					retAttr = attr;
					break;
				}
			}

			if(retAttr != null) {
				ownerDocument.onNodeRemoving (node, ownerElement);
				base.RemoveNamedItem (retAttr.LocalName, retAttr.NamespaceURI);
				RemoveIdenticalAttribute (retAttr);
				ownerDocument.onNodeRemoved (node, ownerElement);
			}
			// If it is default, then directly create new attribute.
			DTDAttributeDefinition def = retAttr.GetAttributeDefinition ();
			if (def != null && def.DefaultValue != null) {
				XmlAttribute attr = ownerDocument.CreateAttribute (
					retAttr.Prefix, retAttr.LocalName, retAttr.NamespaceURI);
				attr.Value = def.DefaultValue;
				attr.SetDefault ();
				this.SetNamedItem (attr);
			}
			retAttr.AttributeOwnerElement = null;
			return retAttr;
		}

#if NET_2_0
		public void RemoveAll () 
#else
		public virtual void RemoveAll () 
#endif
		{
			int current = 0;
			while (current < Count) {
				XmlAttribute attr = this [current];
				if (!attr.Specified)
					current++;
				// It is called for the purpose of event support.
				Remove (attr);
			}
		}

#if NET_2_0
		public XmlAttribute RemoveAt (int i) 
#else
		public virtual XmlAttribute RemoveAt (int i) 
#endif
		{
			if(Count <= i)
				return null;
			return Remove ((XmlAttribute)Nodes [i]);
		}

		public override XmlNode SetNamedItem (XmlNode node)
		{
			if(IsReadOnly)
				throw new ArgumentException ("this AttributeCollection is read only.");

			XmlAttribute attr = node as XmlAttribute;
			if (attr.OwnerElement != null)
				throw new ArgumentException ("This attribute is already set to another element.");

			ownerElement.OwnerDocument.onNodeInserting (node, ownerElement);

			attr.AttributeOwnerElement = ownerElement;
			XmlNode n = base.SetNamedItem (node, -1, false);
			AdjustIdenticalAttributes (node as XmlAttribute, n == node ? null : n);

			ownerElement.OwnerDocument.onNodeInserted (node, ownerElement);

			return n as XmlAttribute;
		}

		internal void AddIdenticalAttribute ()
		{
			SetIdenticalAttribute (false);
		}

		internal void RemoveIdenticalAttribute ()
		{
			SetIdenticalAttribute (true);
		}

		private void SetIdenticalAttribute (bool remove)
		{
			if (ownerElement == null)
				return;

			// Check if new attribute's datatype is ID.
			XmlDocumentType doctype = ownerDocument.DocumentType;
			if (doctype == null || doctype.DTD == null)
				return;
			DTDElementDeclaration elem = doctype.DTD.ElementDecls [ownerElement.Name];
			for (int i = 0; i < Count; i++) {
				XmlAttribute node = (XmlAttribute) Nodes [i];
				DTDAttributeDefinition attdef = elem == null ? null : elem.Attributes [node.Name];
				if (attdef == null || attdef.Datatype.TokenizedType != XmlTokenizedType.ID)
					continue;

				if (remove) {
					if (ownerDocument.GetIdenticalAttribute (node.Value) != null) {
						ownerDocument.RemoveIdenticalAttribute (node.Value);
						return;
					}
				} else {
					// adding new identical attribute, but 
					// MS.NET is pity for ID support, so I'm wondering how to correct it...
					if (ownerDocument.GetIdenticalAttribute (node.Value) != null)
						throw new XmlException (String.Format (
							"ID value {0} already exists in this document.", node.Value));
					ownerDocument.AddIdenticalAttribute (node);
					return;
				}
			}

		}

		private void AdjustIdenticalAttributes (XmlAttribute node, XmlNode existing)
		{
			// If owner element is not appended to the document,
			// ID table should not be filled.
			if (ownerElement == null)
				return;

			if (existing != null)
				RemoveIdenticalAttribute (existing);

			// Check if new attribute's datatype is ID.
			XmlDocumentType doctype = node.OwnerDocument.DocumentType;
			if (doctype == null || doctype.DTD == null)
				return;
			DTDAttListDeclaration attList = doctype.DTD.AttListDecls [ownerElement.Name];
			DTDAttributeDefinition attdef = attList == null ? null : attList.Get (node.Name);
			if (attdef == null || attdef.Datatype.TokenizedType != XmlTokenizedType.ID)
				return;

			// adding new identical attribute, but 
			// MS.NET is pity for ID support, so I'm wondering how to correct it...
			if (ownerDocument.GetIdenticalAttribute (node.Value) != null)
				throw new XmlException (String.Format (
					"ID value {0} already exists in this document.", node.Value));
			ownerDocument.AddIdenticalAttribute (node);
		}

		private XmlNode RemoveIdenticalAttribute (XmlNode existing)
		{
			// If owner element is not appended to the document,
			// ID table should not be filled.
			if (ownerElement == null)
				return existing;

			if (existing != null) {
				// remove identical attribute (if it is).
				if (ownerDocument.GetIdenticalAttribute (existing.Value) != null)
					ownerDocument.RemoveIdenticalAttribute (existing.Value);
			}

			return existing;
		}
	}
}
