//
// System.Runtime.Remoting.MetadataServices.MetaDataCodeGenerator
//
// Authors:
//		Lluis Sanchez Gual (lluis@ximian.com)
//
// (C) 2003 Novell, Inc
//

//
// 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.Collections;
using System.IO;
using System.Xml;
using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Metadata;

namespace System.Runtime.Remoting.MetadataServices
{
	internal class MetaDataCodeGenerator
	{
		XmlDocument doc;
		CodeFile currentFile;
		XmlNamespaceManager nsManager;
		Hashtable sudsTypes;
		
		public void GenerateCode (bool clientProxy, string outputDirectory, Stream inputStream, 
			ArrayList outCodeStreamList, string proxyUrl, string proxyNamespace)
		{
			doc = new XmlDocument ();
			doc.Load (inputStream);
			
			nsManager = new XmlNamespaceManager (doc.NameTable);
			nsManager.AddNamespace ("wsdl", MetaData.WsdlNamespace);
			nsManager.AddNamespace ("s", MetaData.SchemaNamespace);
			nsManager.AddNamespace ("suds", MetaData.SudsNamespace);
		
			if (outputDirectory == null) outputDirectory = Directory.GetCurrentDirectory();
			
			CodeFile mainFile = new CodeFile (outputDirectory);
			
			currentFile = mainFile;
			
			// Suds types
			
			sudsTypes = new Hashtable ();
			XmlNodeList nodes = doc.DocumentElement.SelectNodes ("wsdl:binding/suds:class|wsdl:binding/suds:interface|wsdl:binding/suds:struct", nsManager);
			foreach (XmlElement node in nodes)
				sudsTypes [GetTypeQualifiedName (node, node.GetAttribute ("type"))] = node;
			
			// Data types
			
			nodes = doc.SelectNodes ("wsdl:definitions/wsdl:types/s:schema", nsManager);
			foreach (XmlElement schema in nodes)
				GenerateSchemaCode (schema);
			
			// Services
			
			nodes = doc.SelectNodes ("wsdl:definitions/wsdl:service/wsdl:port", nsManager);
			foreach (XmlElement port in nodes)
				GeneratePortCode (port);
			
			mainFile.Write ();
			if (mainFile.FileName != null)
				outCodeStreamList.Add (mainFile.FilePath);
		}
		
		void GeneratePortCode (XmlElement port)
		{
			XmlElement binding = GetBinding (port.GetAttribute ("binding"));
			XmlElement type = null;
			foreach (XmlNode node in binding)
				if ((node is XmlElement) && ((XmlElement)node).NamespaceURI == MetaData.SudsNamespace) 
				{ type = (XmlElement) node; break; }
					
			string rootType = type.GetAttribute ("rootType");
			if (rootType == "Delegate")
				GenerateServiceDelegateCode (port, binding, type);
			else
				GenerateServiceClassCode (port, binding, type);
		}
		
		void GenerateServiceDelegateCode (XmlElement port, XmlElement binding, XmlElement type)
		{
			string typeName = (type != null) ? type.GetAttribute ("type") : port.GetAttribute ("name");
			string portName = GetNameFromQn (binding.GetAttribute ("type"));
			
			string name, ns;
			GetTypeQualifiedName (port, typeName, out name, out ns);
			currentFile.SetCurrentNamespace (ns);
			
			XmlElement oper = (XmlElement) binding.SelectSingleNode ("wsdl:operation[@name='Invoke']", nsManager);
			if (oper == null) throw new InvalidOperationException ("Invalid delegate schema");
			
			string parsDec;
			string returnType;
			GetParameters (oper, portName, "Invoke", out parsDec, out returnType);

			currentFile.WriteLine ("public delegate " + returnType + " " + name + " (" + parsDec + ");");
			currentFile.WriteLine ("");
		}
		
		void GenerateServiceClassCode (XmlElement port, XmlElement binding, XmlElement type)
		{
			string typeName = (type != null) ? type.GetAttribute ("type") : port.GetAttribute ("name");
			
			string name, ns;
			GetTypeQualifiedName (port, typeName, out name, out ns);
			currentFile.SetCurrentNamespace (ns);
			
			string cls = "public " + type.LocalName + " " + name;
			string baset = type.GetAttribute ("extends");
			if (baset != "") cls += ": " + GetTypeQualifiedName (port, baset);
			
			// Interfaces
			
			XmlNodeList interfaces = type.SelectNodes ("suds:implements",nsManager);
			if (interfaces.Count == 0) interfaces = type.SelectNodes ("suds:extends",nsManager);
			
			foreach (XmlElement interf in interfaces)
			{
				string iname = GetTypeQualifiedName (interf, interf.GetAttribute ("type"));
				if (cls.IndexOf (':') == -1)  cls += ": " + iname;
				else cls += ", " + iname;
			}
			
			currentFile.WriteLine (cls);
			currentFile.WriteLineInd ("{");
			
			string portName = GetNameFromQn (binding.GetAttribute ("type"));
			bool isInterface = type.LocalName == "interface";
			
			string vis = isInterface? "":"public ";
			
			ArrayList mets = GetMethods (portName, binding);
			foreach (MethodData met in mets)
			{
				if (met.IsProperty)
				{
					string prop = vis + met.ReturnType + " ";
					if (met.Signature != "") prop += "this [" + met.Signature + "]";
					else prop += met.Name;
					
					if (isInterface)
					{
						prop += " { ";
						if (met.HasGet) prop += "get; ";
						if (met.HasSet) prop += "set; ";
						prop += "}";
						currentFile.WriteLine (prop);
					}
					else
					{
						currentFile.WriteLine (prop);
						currentFile.WriteLineInd ("{");
						if (met.HasGet) currentFile.WriteLine ("get { throw new NotImplementedException (); }");
						if (met.HasSet) currentFile.WriteLine ("set { throw new NotImplementedException (); }");
						currentFile.WriteLineUni ("}");
						currentFile.WriteLine ("");
					}
				}
				else
				{
					currentFile.WriteLine (vis + met.ReturnType + " " + met.Name + " (" + met.Signature + ")" + (isInterface?";":""));
					if (!isInterface)
					{
						currentFile.WriteLineInd ("{");
						currentFile.WriteLine ("throw new NotImplementedException ();");
						currentFile.WriteLineUni ("}");
						currentFile.WriteLine ("");
					}
				}
			}
			
			currentFile.WriteLineUni ("}");
			currentFile.WriteLine ("");
		}
		
		class MethodData
		{
			public string ReturnType;
			public string Signature;
			public string Name;
			public bool HasSet;
			public bool HasGet;
			
			public bool IsProperty { get { return HasGet || HasSet; } }
		}
		
		ArrayList GetMethods (string portName, XmlElement binding)
		{
			ArrayList mets = new ArrayList ();
			
			XmlNodeList nodes = binding.SelectNodes ("wsdl:operation", nsManager);
			foreach (XmlElement oper in nodes)
			{
				MethodData md = new MethodData ();
				md.Name = oper.GetAttribute ("name");
				
				GetParameters (oper, portName, md.Name, out md.Signature, out md.ReturnType);
				
				if (md.Name.StartsWith ("set_") || md.Name.StartsWith ("get_"))
				{
					string tmp = ", " + md.Signature;
					if (tmp.IndexOf (", out ") == -1 && tmp.IndexOf (", ref ") == -1)
					{
						bool isSet = md.Name[0]=='s';
						md.Name = md.Name.Substring (4);
						MethodData previousProp = null;
						
						foreach (MethodData fmd in mets)
							if (fmd.Name == md.Name && fmd.IsProperty)
								previousProp = fmd;
								
						if (previousProp != null) {
							if (isSet) previousProp.HasSet = true;
							else { previousProp.HasGet = true; previousProp.Signature = md.Signature; }
							continue;
						}
						else {
							if (isSet) { md.HasSet = true; md.Signature = ""; }
							else md.HasGet = true;
						}
					}
				}
				
				mets.Add (md);
			}
			return mets;
		}
		
		void GetParameters (XmlElement oper, string portName, string operName, out string signature, out string returnType)
		{
			returnType = null;
			
			XmlElement portType = (XmlElement) doc.SelectSingleNode ("wsdl:definitions/wsdl:portType[@name='" + portName + "']", nsManager);
			XmlElement portOper = (XmlElement) portType.SelectSingleNode ("wsdl:operation[@name='" + operName + "']", nsManager);
			string[] parNames = portOper.GetAttribute ("parameterOrder").Split (' ');
			
			XmlElement inPortMsg = (XmlElement) portOper.SelectSingleNode ("wsdl:input", nsManager);
			XmlElement inMsg = FindMessageFromPortMessage (inPortMsg);
			
			XmlElement outPortMsg = (XmlElement) portOper.SelectSingleNode ("wsdl:output", nsManager);
			XmlElement outMsg = FindMessageFromPortMessage (outPortMsg);

			string[] parameters;
			if (parNames [0] != "") parameters = new string [parNames.Length];
			else parameters = new string [0];
			
			foreach (XmlElement part in inMsg.SelectNodes ("wsdl:part",nsManager))
			{
				int i = Array.IndexOf (parNames, part.GetAttribute ("name"));
				string type = GetTypeQualifiedName (part, part.GetAttribute ("type"));
				parameters [i] = type + " " + parNames [i];
			}
			
			foreach (XmlElement part in outMsg.SelectNodes ("wsdl:part",nsManager))
			{
				string pn = part.GetAttribute ("name");
				string type = GetTypeQualifiedName (part, part.GetAttribute ("type"));
				
				if (pn == "return") 
					returnType = type;
				else {
					int i = Array.IndexOf (parNames, pn);
					if (parameters [i] != null) parameters [i] = "ref " + parameters [i];
					else parameters [i] = "out " + type + " " + pn;
				}
			}
			
			signature = string.Join (", ", parameters);
			if (returnType == null) returnType = "void";
		}
		
		XmlElement FindMessageFromPortMessage (XmlElement portMsg)
		{
			string msgName = portMsg.GetAttribute ("message");
			msgName = GetNameFromQn (msgName);
			return (XmlElement) doc.SelectSingleNode ("wsdl:definitions/wsdl:message[@name='" + msgName + "']", nsManager);
		}
		
		void GenerateSchemaCode (XmlElement schema)
		{
			string ns = schema.GetAttribute ("targetNamespace");
			string clrNs = DecodeNamespace (ns);
			currentFile.SetCurrentNamespace (clrNs);
			
			foreach (XmlNode node in schema)
			{
				XmlElement elem = node as XmlElement;
				if (elem == null) continue;
				
				if (elem.LocalName == "complexType")
					GenerateClassCode (ns, elem);
				else if (elem.LocalName == "simpleType")
					GenerateEnumCode (ns, elem);
			}
		}
		
		void GenerateClassCode (string ns, XmlElement elem)
		{
			if (elem.SelectSingleNode ("s:complexContent/s:restriction", nsManager) != null) return;
			string clrNs = DecodeNamespace (ns);
			string typeName = GetTypeName (elem.GetAttribute ("name"), ns);
			
			XmlElement sudsType = (XmlElement) sudsTypes [clrNs + "." + typeName];
			
			string typetype = "class";
			if (sudsType != null) typetype = sudsType.LocalName;
			
			currentFile.WriteLine ("[Serializable, SoapType (XmlNamespace = @\"" + ns + "\", XmlTypeNamespace = @\"" + ns + "\")]");
			
			string cls = "public " + typetype + " " + typeName;
			string baseType = elem.GetAttribute ("base");
			if (baseType != "") cls += ": " + GetTypeQualifiedName (elem, baseType);
			
			bool isSerializable = (sudsType.GetAttribute ("rootType") == "ISerializable");
			
			if (isSerializable)
			{
				if (cls.IndexOf (':') == -1) cls += ": ";
				else cls += ", ";
				cls += "System.Runtime.Serialization.ISerializable";
			}
			
			currentFile.WriteLine (cls);
			currentFile.WriteLineInd ("{");
			
			XmlNodeList elems = elem.GetElementsByTagName ("element", MetaData.SchemaNamespace);
			foreach (XmlElement elemField in elems)
				WriteField (elemField);
			
			elems = elem.GetElementsByTagName ("attribute", MetaData.SchemaNamespace);
			foreach (XmlElement elemField in elems)
				WriteField (elemField);
			
			if (isSerializable)
			{
				currentFile.WriteLine ("");
				currentFile.WriteLine ("public " + typeName + " ()");
				currentFile.WriteLineInd ("{");
				currentFile.WriteLineUni ("}");
				currentFile.WriteLine ("");
				
				currentFile.WriteLine ("public " + typeName + " (System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)");
				currentFile.WriteLineInd ("{");
				currentFile.WriteLine ("throw new NotImplementedException ();");
				currentFile.WriteLineUni ("}");
				currentFile.WriteLine ("");
				
				currentFile.WriteLine ("public void GetObjectData (System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)");
				currentFile.WriteLineInd ("{");
				currentFile.WriteLine ("throw new NotImplementedException ();");
				currentFile.WriteLineUni ("}");
			}
			
			currentFile.WriteLineUni ("}");
			currentFile.WriteLine ("");
		}
		
		void WriteField (XmlElement elemField)
		{
			bool isAttr = elemField.LocalName == "attribute";
			
			string type = elemField.GetAttribute ("type");
			
			if (isAttr)
				currentFile.WriteLine ("[SoapField (UseAttribute = true)]");
			else if (!IsPrimitive (elemField, type))
				currentFile.WriteLine ("[SoapField (Embedded = true)]");
			currentFile.WriteLine ("public " + GetTypeQualifiedName (elemField, type) + " " + elemField.GetAttribute ("name") + ";");
		}
		
		void GenerateEnumCode (string ns, XmlElement elem)
		{
			currentFile.WriteLine ("public enum " + GetTypeName (elem.GetAttribute ("name"), ns));
			currentFile.WriteLineInd ("{");
			
			XmlNodeList nodes = elem.SelectNodes ("s:restriction/s:enumeration/@value", nsManager);
			foreach (XmlNode node in nodes)
				currentFile.WriteLine (node.Value + ",");
				
			currentFile.WriteLineUni ("}");
			currentFile.WriteLine ("");
		}
		
		bool IsPrimitive (XmlNode node, string qname)
		{
			string name = GetTypeQualifiedName (node, qname);
			return name.IndexOf ('.') == -1;
		}
		
		string GetTypeName (string localName, string ns)
		{
			return localName;
		}
		
		void GetTypeQualifiedName (XmlNode node, string qualifiedName, out string name, out string ns)
		{
			int i = qualifiedName.IndexOf (':');
			if (i == -1)
			{
				name = qualifiedName;
				ns = "";
				return;
			}
			
			string prefix = qualifiedName.Substring (0,i);
			name = qualifiedName.Substring (i+1);
			ns = node.GetNamespaceOfPrefix (prefix);
			
			string arrayType = GetArrayType (node, name, ns);
			if (arrayType != null) {
				name = arrayType;
				ns = "";
			}
			else if (ns != MetaData.SchemaNamespace) {
				ns = DecodeNamespace (ns);
			}
			else {
				ns = "";
				name = GetClrFromXsd (name);
			}
		}
		
		string GetClrFromXsd (string type)
		{
			switch (type)
			{
				case "boolean": return "bool";
				case "unsignedByte": return "byte";
				case "char": return "char";
				case "dateTime": return "DateTime";
				case "decimal": return "decimal";
				case "double": return "double";
				case "short": return "short";
				case "int": return "int";
				case "long": return "long";
				case "byte": return "sbyte";
				case "float": return "float";
				case "unsignedShort": return "ushort";
				case "unsignedInt": return "uint";
				case "unsignedLong": return "ulong";
				case "string": return "string";
				case "duration": return "TimeSpan";
				case "anyType": return "object";
			}
			throw new InvalidOperationException ("Unknown schema type: " + type);
		}
		
		string GetTypeQualifiedName (XmlNode node, string qualifiedName)
		{
			string name, ns;
			GetTypeQualifiedName (node, qualifiedName, out name, out ns);
			if (ns != "") return ns + "." + name;
			else return name;
		}
		
		string GetTypeNamespace (XmlNode node, string qualifiedName)
		{
			string name, ns;
			GetTypeQualifiedName (node, qualifiedName, out name, out ns);
			return ns;
		}
		
		string GetArrayType (XmlNode node, string name, string ns)
		{
			XmlNode anod = doc.SelectSingleNode ("wsdl:definitions/wsdl:types/s:schema[@targetNamespace='" + ns + "']/s:complexType[@name='" + name + "']/s:complexContent/s:restriction/s:attribute/@wsdl:arrayType", nsManager);
			if (anod == null) return null;
			
			string atype = anod.Value;
			int i = atype.IndexOf ('[');
			string itemType = GetTypeQualifiedName (node, atype.Substring (0,i));
			
			return itemType + atype.Substring (i);
		}
		
		XmlElement GetBinding (string name)
		{
			int i = name.IndexOf (':');
			name = name.Substring (i+1);
			return doc.SelectSingleNode ("wsdl:definitions/wsdl:binding[@name='" + name + "']", nsManager) as XmlElement;
		}
		
		string DecodeNamespace (string xmlNamespace)
		{
			string tns, tasm;
			
			if (!SoapServices.DecodeXmlNamespaceForClrTypeNamespace (xmlNamespace, out tns, out tasm))
				tns = xmlNamespace;
				
			return tns;
		}
		
		string GetLiteral (object ob)
		{
			if (ob == null) return "null";
			if (ob is string) return "\"" + ob.ToString().Replace("\"","\"\"") + "\"";
			if (ob is bool) return ((bool)ob) ? "true" : "false";
			if (ob is XmlQualifiedName) {
				XmlQualifiedName qn = (XmlQualifiedName)ob;
				return "new XmlQualifiedName (" + GetLiteral(qn.Name) + "," + GetLiteral(qn.Namespace) + ")";
			}
			else return ob.ToString ();
		}
		
		string Params (params string[] pars)
		{
			string res = "";
			foreach (string p in pars)
			{
				if (res != "") res += ", ";
				res += p;
			}
			return res;
		}
		
		string GetNameFromQn (string qn)
		{
			int i = qn.IndexOf (':');
			if (i == -1) return qn;
			else return qn.Substring (i+1);
		}
	}
	
	class CodeFile
	{
		public string FileName;
		public string Directory;
		public string FilePath;
		Hashtable namespaces = new Hashtable ();
		public StringWriter writer;
		int indent;
		
		string currentNamespace;
		
		public CodeFile (string directory)
		{
			Directory = directory;
		}
		
		public void SetCurrentNamespace (string ns)
		{
			writer = namespaces [ns] as StringWriter;
			if (writer == null)
			{
				indent = 0;
				writer = new StringWriter ();
				namespaces [ns] = writer;
				WriteLine ("namespace " + ns);
				WriteLineInd ("{");
			}
			
			indent = 1;
			
			if (FileName == null)
				FileName = ns + ".cs";
		}
		
		public void WriteLineInd (string code)
		{
			WriteLine (code);
			indent++;
		}
		
		public void WriteLineUni (string code)
		{
			if (indent > 0) indent--;
			WriteLine (code);
		}
		
		public void WriteLine (string code)
		{
			if (code != "")	writer.Write (new String ('\t',indent));
			writer.WriteLine (code);
		}
		
		public void Write ()
		{
			if (FileName == null) return;
			
			FilePath = Path.Combine (Directory, FileName);
			StreamWriter sw = new StreamWriter (FilePath);
			
			sw.WriteLine ("using System;");
			sw.WriteLine ("using System.Runtime.Remoting.Metadata;");
			sw.WriteLine ();
			
			foreach (StringWriter nsWriter in namespaces.Values)
			{
				sw.Write (nsWriter.ToString ());
				sw.WriteLine ("}");
				sw.WriteLine ();
			}
				
			sw.Close ();
		}
	}
	
	
}

