
//
// 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.Xml;
using System.Collections;
using System.Text;
using Mono.Xml;
using Mono.Xml.Schema;
using System.Xml.Serialization;

namespace System.Xml.Schema
{
	/// <summary>
	///  All Methods in this class should use XmlConvert. Some Methods are not present in the
	///  MS Implementation. We should provide them.
	/// </summary>
	internal class XmlSchemaUtil
	{
		static XmlSchemaUtil ()
		{
			FinalAllowed = XmlSchemaDerivationMethod.Restriction | 
				XmlSchemaDerivationMethod.Extension;
			ComplexTypeBlockAllowed = FinalAllowed;
			ElementBlockAllowed = XmlSchemaDerivationMethod.Substitution | 
				FinalAllowed;
		}

		internal static XmlSchemaDerivationMethod FinalAllowed;
		internal static XmlSchemaDerivationMethod ElementBlockAllowed;
		internal static XmlSchemaDerivationMethod ComplexTypeBlockAllowed;


		public static void AddToTable (XmlSchemaObjectTable table, XmlSchemaObject obj,
			XmlQualifiedName qname, ValidationEventHandler h)
		{
			if (table.Contains (qname)) {
				// FIXME: This logic unexpectedly allows 
				// one redefining item and two or more redefining items.
				// FIXME: redefining item is not simple replacement,
				// but much more complex stuff.
				if (obj.isRedefineChild) {	// take precedence.
					if (obj.redefinedObject != null)
						obj.error (h, String.Format ("Named item {0} was already contained in the schema object table.", qname));
					else
						obj.redefinedObject = table [qname];
					table.Set (qname, obj);
				}
				else if (table [qname].isRedefineChild) {
					if (table [qname].redefinedObject != null)
						obj.error (h, String.Format ("Named item {0} was already contained in the schema object table.", qname));
					else
						table [qname].redefinedObject = obj;
					return;	// never add to the table.
				}
				else
					obj.error (h, String.Format ("Named item {0} was already contained in the schema object table.", qname));
			}
			else
				table.Set (qname, obj);
		}

		public static void CompileID (string id,  XmlSchemaObject xso, Hashtable idCollection, ValidationEventHandler h)
		{
			//check if the string conforms to http://www.w3.org/TR/2001/REC-xmlschema-2-20010502/datatypes.html#ID
			// 1. ID must be a NCName
			// 2. ID must be unique in the schema
			if(id == null)
				return;
			if(!CheckNCName(id)) 
				xso.error(h,id+" is not a valid id attribute");
			else if(idCollection.ContainsKey(id))
				xso.error(h,"Duplicate id attribute "+id);
			else
				idCollection.Add(id,xso);
		}

		public static bool CheckAnyUri (string uri)
		{
			if (uri.StartsWith ("##"))
				return false;
			return true;
		}

		public static bool CheckNormalizedString (string token)
		{
			return true;
		}

		public static bool CheckNCName (string name)
		{
			//check if the string conforms to http://www.w3.org/TR/2001/REC-xmlschema-2-20010502/datatypes.html#NCName
			return XmlChar.IsNCName (name);
		}

		public static bool CheckQName (XmlQualifiedName qname)
		{
			// What is this doing?
			return true;
		}

		public static XmlParserContext GetParserContext (XmlReader reader)
		{
			IHasXmlParserContext xctx = reader as IHasXmlParserContext;
			if (xctx != null)
				return xctx.ParserContext;

			return null;
		}

		public static bool IsBuiltInDatatypeName (XmlQualifiedName qname)
		{
			if (qname.Namespace == XmlSchema.XdtNamespace) {
				switch (qname.Name) {
				case "anyAtomicType":
				case "untypedAtomic":
				case "dayTimeDuration":
				case "yearMonthDuration":
					return true;
				default:
					return false;
				}
			}
			if (qname.Namespace != XmlSchema.Namespace)
				return false;
			switch (qname.Name) {
			case "anySimpleType":
			case "duration": case "dateTime": case "time":
			case "date": case "gYearMonth": case "gYear":
			case "gMonthDay": case "gDay": case "gMonth":
			case "boolean":
			case "base64Binary": case "hexBinary":
			case "float": case "double":
			case "anyURI":
			case "QName":
			case "NOTATION":
			case "string": case "normalizedString": case "token":
			case "language": case "Name": case "NCName":
			case "ID": case "IDREF": case "IDREFS":
			case "ENTITY": case "ENTITIES":
			case "NMTOKEN": case "NMTOKENS":
			case "decimal": case "integer":
			case "nonPositiveInteger": case "negativeInteger":
			case "nonNegativeInteger":
			case "unsignedLong": case "unsignedInt":
			case "unsignedShort": case "unsignedByte":
			case "positiveInteger":
			case "long": case "int": case "short": case "byte":
				return true;
			}
			return false;
		}

		public static bool IsSchemaDatatypeEquals (XsdAnySimpleType st1, object v1,
			XsdAnySimpleType st2, object v2)
		{
			if (v1 == null || v2 == null)
				return false;

			if (st1 == null)
				st1 = XmlSchemaSimpleType.AnySimpleType;
			if (st2 == null)
				st2 = XmlSchemaSimpleType.AnySimpleType;

			Type t = st2.GetType ();
			if (st1 is XsdFloat) {
				return st2 is XsdFloat && Convert.ToSingle (v1) == Convert.ToSingle (v2);
			} else if (st1 is XsdDouble) {
				return st2 is XsdDouble && Convert.ToDouble (v1) == Convert.ToDouble (v2);
			} else if (st1 is XsdDecimal) {
				if (!(st2 is XsdDecimal) || Convert.ToDecimal (v1) != Convert.ToDecimal (v2))
					return false;
				if (st1 is XsdNonPositiveInteger)
					return st2 is XsdNonPositiveInteger || t == typeof (XsdDecimal) || t == typeof (XsdInteger);
				else if (st1 is XsdPositiveInteger)
					return st2 is XsdPositiveInteger || t == typeof (XsdDecimal) || 
						t == typeof (XsdInteger) || t == typeof (XsdNonNegativeInteger);
				else if (st1 is XsdUnsignedLong)
					return st2 is XsdUnsignedLong || t == typeof (XsdDecimal) || 
						t == typeof (XsdInteger) || t == typeof (XsdNonNegativeInteger);
				else if (st1 is XsdNonNegativeInteger)
					return st2 is XsdNonNegativeInteger || t == typeof (XsdDecimal) || t == typeof (XsdInteger);
				else if (st1 is XsdLong)
					return st2 is XsdLong || t == typeof (XsdDecimal) || t == typeof (XsdInteger);
				return true;
			}
			else if (!v1.Equals (v2))
				return false;
			if (st1 is XsdString) {
				if (!(st2 is XsdString))
					return false;
				if (st1 is XsdNMToken && (st2 is XsdLanguage || st2 is XsdName))
					return false;
				if (st2 is XsdNMToken && (st1 is XsdLanguage || st1 is XsdName))
					return false;
				if (st1 is XsdName && (st2 is XsdLanguage || st2 is XsdNMToken))
					return false;
				if (st2 is XsdName && (st1 is XsdLanguage || st1 is XsdNMToken))
					return false;
				if (st1 is XsdID && st2 is XsdIDRef)
					return false;
				if (st1 is XsdIDRef && st2 is XsdID)
					return false;
			}
			else if (st1 != st2)
				return false;
			return true;
		}

		public static bool IsValidQName(string qname)
		{
			foreach(string part in qname.Split(new char[]{':'},2))
			{
				if(!CheckNCName(part))
					return false;
			}
			return true;
		}

		//FIXME: First remove all the multiple instances of whitespace and then return the strings.
		//The current method returns empty strings if there are two or more consecutive whitespaces.
		public static string[] SplitList(string list)
		{
			if(list == null || list == string.Empty)
				return new string [0];

			ArrayList al = null;
			int start = 0;
			bool wait = true;
			for (int i = 0; i < list.Length; i++) {
				switch (list [i]) {
				case ' ':
				case '\r':
				case '\n':
				case '\t':
					if (!wait) {
						if (al == null)
							al = new ArrayList ();
						al.Add (list.Substring (start, i - start));
					}
					wait = true;
					break;
				default:
					if (wait) {
						wait = false;
						start = i;
					}
					break;
				}
			}

			if (!wait && start == 0)
				return new string [] {list};

			if (!wait && start < list.Length)
				al.Add (start == 0 ? list : list.Substring (start));
			return al.ToArray (typeof (string)) as string [];
		}

		public static void ReadUnhandledAttribute(XmlReader reader, XmlSchemaObject xso)
		{
			if(reader.Prefix == "xmlns")
				xso.Namespaces.Add(reader.LocalName, reader.Value);
			else if(reader.Name == "xmlns")
				xso.Namespaces.Add("",reader.Value);
			else
			{
				if(xso.unhandledAttributeList == null)
					xso.unhandledAttributeList = new System.Collections.ArrayList();
				XmlAttribute attr = new XmlDocument().CreateAttribute(reader.LocalName,reader.NamespaceURI);
				attr.Value = reader.Value;
				ParseWsdlArrayType (reader, attr);
				xso.unhandledAttributeList.Add(attr);
			}
		}
		
		static void ParseWsdlArrayType (XmlReader reader, XmlAttribute attr)
		{
			if (attr.NamespaceURI == XmlSerializer.WsdlNamespace && attr.LocalName == "arrayType")
			{
				string ns = "", type, dimensions;
				TypeTranslator.ParseArrayType (attr.Value, out type, out ns, out dimensions);
				if (ns != "") ns = reader.LookupNamespace (ns) + ":";
				attr.Value = ns + type + dimensions;
			}
		}

		public static bool ReadBoolAttribute(XmlReader reader, out Exception innerExcpetion)
		{
			innerExcpetion = null;
			try
			{
				bool val = XmlConvert.ToBoolean(reader.Value);
				return val;
			}
			catch(Exception ex)
			{
				innerExcpetion = ex;
				return false;
			}
		}
		public static decimal ReadDecimalAttribute(XmlReader reader,  out Exception innerExcpetion)
		{
			innerExcpetion = null;
			try
			{
				decimal val = XmlConvert.ToDecimal(reader.Value);
				return val;
			}
			catch(Exception ex)
			{
				innerExcpetion = ex;
				return decimal.Zero;
			}
		}

		// Is some value is read, return it.
		// If no values return empty.
		// If exception, return none
		public static XmlSchemaDerivationMethod ReadDerivationAttribute(XmlReader reader, out Exception innerExcpetion, string name, XmlSchemaDerivationMethod allowed)
		{
			innerExcpetion = null;
			try
			{
				string list = reader.Value;
				string warn = "";
				XmlSchemaDerivationMethod val = 0;
				
				if(list.IndexOf("#all") != -1 && list.Trim() != "#all")
				{
					innerExcpetion = new Exception(list+" is not a valid value for "+ name +". #all if present must be the only value");
					return XmlSchemaDerivationMethod.All;
				}
				foreach(string xsdm in XmlSchemaUtil.SplitList(list))
				{
					switch(xsdm)
					{
						case "":
							val = AddFlag (val, XmlSchemaDerivationMethod.Empty, allowed); break;
						case "#all":
							val = AddFlag (val,XmlSchemaDerivationMethod.All, allowed); break;
						case "substitution":
							val = AddFlag (val,XmlSchemaDerivationMethod.Substitution, allowed); break;
						case "extension":
							val = AddFlag (val,XmlSchemaDerivationMethod.Extension, allowed); break;
						case "restriction":
							val = AddFlag (val,XmlSchemaDerivationMethod.Restriction, allowed); break;
						case "list":
							val = AddFlag (val,XmlSchemaDerivationMethod.List, allowed); break;
						case "union":
							val = AddFlag (val,XmlSchemaDerivationMethod.Union, allowed); break;
						default:
							warn += xsdm + " "; break;
					}
				}
				if(warn != "")
						innerExcpetion = new Exception(warn + "is/are not valid values for " + name);
				return val;
			}
			catch(Exception ex)
			{
				innerExcpetion = ex;
				return XmlSchemaDerivationMethod.None;
			}
		}

		private static XmlSchemaDerivationMethod AddFlag (XmlSchemaDerivationMethod dst,
			XmlSchemaDerivationMethod add, XmlSchemaDerivationMethod allowed)
		{
			if ((add & allowed) == 0 && allowed != XmlSchemaDerivationMethod.All)
				throw new ArgumentException (add + " is not allowed in this attribute.");
			if ((dst & add) != 0)
				throw new ArgumentException (add + " is already specified in this attribute.");
			return dst | add;
		}

		public static XmlSchemaForm ReadFormAttribute(XmlReader reader, out Exception innerExcpetion)
		{
			innerExcpetion = null;
			XmlSchemaForm val = XmlSchemaForm.None;
			switch(reader.Value)
			{
				case "qualified":
					val = XmlSchemaForm.Qualified; break;
				case "unqualified":
					val = XmlSchemaForm.Unqualified; break;
				default:
					innerExcpetion = new Exception("only qualified or unqulified is a valid value"); break;
			}
			return val;
		}

		public static XmlSchemaContentProcessing ReadProcessingAttribute(XmlReader reader, out Exception innerExcpetion)
		{
			innerExcpetion = null;
			XmlSchemaContentProcessing val = XmlSchemaContentProcessing.None;
			switch(reader.Value)
			{
				case "lax":
					val = XmlSchemaContentProcessing.Lax; break;
				case "strict":
					val = XmlSchemaContentProcessing.Strict; break;
				case "skip":
					val = XmlSchemaContentProcessing.Skip; break;
				default:
					innerExcpetion = new Exception("only lax , strict or skip are valid values for processContents");
					break;
			}
			return val;
		}

		public static XmlSchemaUse ReadUseAttribute(XmlReader reader, out Exception innerExcpetion)
		{
			innerExcpetion = null;
			XmlSchemaUse val = XmlSchemaUse.None;
			switch(reader.Value)
			{
				case "optional":
					val = XmlSchemaUse.Optional; break;
				case "prohibited":
					val = XmlSchemaUse.Prohibited; break;
				case "required":
					val = XmlSchemaUse.Required; break;
				default:
					innerExcpetion = new Exception("only optional , prohibited or required are valid values for use");
					break;
			}
			return val;
		}
		public static XmlQualifiedName ReadQNameAttribute(XmlReader reader, out Exception innerEx)
		{
			return ToQName(reader, reader.Value, out innerEx);
		}

		//While Creating a XmlQualifedName, we should check:
		// 1. If a prefix is present, its namespace should be resolvable.
		// 2. If a prefix is not present, and if the defaultNamespace is set, 
		public static XmlQualifiedName ToQName(XmlReader reader, string qnamestr, out Exception innerEx)
		{

			string ns;
			string name;
			XmlQualifiedName qname;
			innerEx = null;
			
			if(!IsValidQName(qnamestr))
			{
				innerEx = new Exception(qnamestr + " is an invalid QName. Either name or namespace is not a NCName");
				return XmlQualifiedName.Empty;
			}

			string[] values = qnamestr.Split(new char[]{':'},2);

			if(values.Length == 2)
			{
				ns = reader.LookupNamespace(values[0]);
				if(ns == null)
				{
					innerEx = new Exception("Namespace Prefix '"+values[0]+"could not be resolved");
					return XmlQualifiedName.Empty;
				}
				name = values[1];
			}
			else
			{
				//Default Namespace
				ns = reader.LookupNamespace("");
				name = values[0];
			}

			qname = new XmlQualifiedName(name,ns);
			return qname;
		}

		public static int ValidateAttributesResolved (
			XmlSchemaObjectTable attributesResolved,
			ValidationEventHandler h,
			XmlSchema schema,
			XmlSchemaObjectCollection attributes,
			XmlSchemaAnyAttribute anyAttribute,
			ref XmlSchemaAnyAttribute anyAttributeUse,
			XmlSchemaAttributeGroup redefined,
			bool skipEquivalent)
		{
			int errorCount = 0;
			if (anyAttribute != null && anyAttributeUse == null)
				anyAttributeUse = anyAttribute;

			ArrayList newAttrNames = new ArrayList ();

			foreach (XmlSchemaObject xsobj in attributes) {
				XmlSchemaAttributeGroupRef grpRef = xsobj as XmlSchemaAttributeGroupRef;
				if (grpRef != null) {
					// Resolve attributeGroup redefinition.
					XmlSchemaAttributeGroup grp = null;
					if (redefined != null && grpRef.RefName == redefined.QualifiedName)
						grp = redefined;
					else
						grp = schema.FindAttributeGroup (grpRef.RefName);
					// otherwise, it might be missing sub components.
					if (grp == null) {
						if (!schema.missedSubComponents)// && schema.Schemas [grpRef.RefName.Namespace] != null)
							grpRef.error (h, "Referenced attribute group " + grpRef.RefName + " was not found in the corresponding schema.");
						continue;
					}
					if (grp.AttributeGroupRecursionCheck) {
						grp.error (h, "Attribute group recursion was found: " + grpRef.RefName);
						continue;
					}
					try {
						grp.AttributeGroupRecursionCheck = true;
						errorCount += grp.Validate (h, schema);
					} finally {
						grp.AttributeGroupRecursionCheck = false;
					}
					if (grp.AnyAttributeUse != null) {
						if (anyAttribute == null)
							anyAttributeUse = grp.AnyAttributeUse;
					}
					foreach (DictionaryEntry entry in grp.AttributeUses) {
						XmlSchemaAttribute attr = (XmlSchemaAttribute) entry.Value;
#if BUGGY_MS_COMPLIANT
						if (attr.Use == XmlSchemaUse.Prohibited)
							continue;
#endif
						if (attr.RefName != null && attr.RefName != XmlQualifiedName.Empty && (!skipEquivalent || !AreAttributesEqual (attr, attributesResolved [attr.RefName] as XmlSchemaAttribute)))
							AddToTable (attributesResolved, attr, attr.RefName, h);
						else if (!skipEquivalent || !AreAttributesEqual (attr, attributesResolved [attr.QualifiedName] as XmlSchemaAttribute))
							AddToTable (attributesResolved, attr, attr.QualifiedName, h);
					}
				} else {
					XmlSchemaAttribute attr = xsobj as XmlSchemaAttribute;
					if (attr != null) {
						errorCount += attr.Validate (h, schema);

						if (newAttrNames.Contains (attr.QualifiedName))
							attr.error (h, String.Format ("Duplicate attributes was found for '{0}'", attr.QualifiedName));
						newAttrNames.Add (attr.QualifiedName);

#if BUGGY_MS_COMPLIANT
						if (attr.Use == XmlSchemaUse.Prohibited)
							continue;
#endif
						if (attr.RefName != null && attr.RefName != XmlQualifiedName.Empty && (!skipEquivalent || !AreAttributesEqual (attr, attributesResolved [attr.RefName] as XmlSchemaAttribute)))
							AddToTable (attributesResolved, attr, attr.RefName, h);
						else if (!skipEquivalent || !AreAttributesEqual (attr, attributesResolved [attr.QualifiedName] as XmlSchemaAttribute))
							AddToTable (attributesResolved, attr, attr.QualifiedName, h);
					} else {
						if (anyAttribute == null) {
							anyAttributeUse = (XmlSchemaAnyAttribute) xsobj;
							anyAttribute.Validate (h, schema);
						}
					}
				}
			}
			return errorCount;
		}

		internal static bool AreAttributesEqual (XmlSchemaAttribute one,
			XmlSchemaAttribute another)
		{
			if (one == null || another == null)
				return false;
			return one.AttributeType == another.AttributeType &&
				one.Form == another.Form &&
				one.ValidatedUse == another.ValidatedUse &&
				one.ValidatedDefaultValue == another.ValidatedDefaultValue &&
				one.ValidatedFixedValue == another.ValidatedFixedValue;
		}

#if NET_2_0
		public static object ReadTypedValue (XmlReader reader,
			object type, IXmlNamespaceResolver nsResolver,
			StringBuilder tmpBuilder)
#else
		public static object ReadTypedValue (XmlReader reader,
			object type, XmlNamespaceManager nsResolver,
			StringBuilder tmpBuilder)
#endif
		{
			if (tmpBuilder == null)
				tmpBuilder = new StringBuilder ();
			XmlSchemaDatatype dt = type as XmlSchemaDatatype;
			XmlSchemaSimpleType st = type as XmlSchemaSimpleType;
			if (st != null)
				dt = st.Datatype;
			if (dt == null)
				return null;

			switch (reader.NodeType) {
			case XmlNodeType.Element:
				if (reader.IsEmptyElement)
					return null;

				tmpBuilder.Length = 0;
				bool loop = true;
				do {
					reader.Read ();
					switch (reader.NodeType) {
					case XmlNodeType.SignificantWhitespace:
					case XmlNodeType.Text:
					case XmlNodeType.CDATA:
						tmpBuilder.Append (reader.Value);
						break;
					case XmlNodeType.Comment:
						break;
					default:
						loop = false;
						break;
					}
				} while (loop && !reader.EOF && reader.ReadState == ReadState.Interactive);
				return dt.ParseValue (tmpBuilder.ToString (), reader.NameTable, nsResolver);
			case XmlNodeType.Attribute:
				return dt.ParseValue (reader.Value, reader.NameTable, nsResolver);
			}
			return null;
		}

		public static XmlSchemaObject FindAttributeDeclaration (
			string ns,
			XmlSchemaSet schemas,
			XmlSchemaComplexType cType,
			XmlQualifiedName qname)
		{
			XmlSchemaObject result = cType.AttributeUses [qname];
			if (result != null)
				return result;
			if (cType.AttributeWildcard == null)
				return null;

			if (!AttributeWildcardItemValid (cType.AttributeWildcard, qname, ns))
				return null;

			if (cType.AttributeWildcard.ResolvedProcessContents == XmlSchemaContentProcessing.Skip)
				return cType.AttributeWildcard;
			XmlSchemaAttribute attr = schemas.GlobalAttributes [qname] as XmlSchemaAttribute;
			if (attr != null)
				return attr;
			if (cType.AttributeWildcard.ResolvedProcessContents == XmlSchemaContentProcessing.Lax)
				return cType.AttributeWildcard;
			else
				return null;
		}

		// Spec 3.10.4 Item Valid (Wildcard)
		private static bool AttributeWildcardItemValid (XmlSchemaAnyAttribute anyAttr, XmlQualifiedName qname, string ns)
		{
			if (anyAttr.HasValueAny)
				return true;
			if (anyAttr.HasValueOther && (anyAttr.TargetNamespace == "" || ns != anyAttr.TargetNamespace))
				return true;
			if (anyAttr.HasValueTargetNamespace && ns == anyAttr.TargetNamespace)
				return true;
			if (anyAttr.HasValueLocal && ns == "")
				return true;
			for (int i = 0; i < anyAttr.ResolvedNamespaces.Count; i++)
				if (anyAttr.ResolvedNamespaces [i] == ns)
					return true;
			return false;
		}
	}
}
