///
/// MonoXSD.cs -- A reflection-based tool for dealing with XML Schema.
///
/// Authors: Duncan Mak (duncan@ximian.com)
///          Lluis Sanchez Gual (lluis@ximian.com)
///          Atsushi Enomoto (atsushi@ximian.com)
///
/// Copyright (C) 2003, Duncan Mak,
///                     Ximian, Inc.
///

using System;
using System.Collections;
using System.Data;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.CodeDom;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using Microsoft.VisualBasic;

namespace Mono.Util {

	public class Driver
	{
		public static readonly string helpString =
			"xsd.exe - a utility for generating schema or class files\n\n" +
			"xsd.exe <schema>.xsd /classes [/element:NAME] [/language:NAME]\n" +
			"            [/namespace:NAME] [/outputdir:PATH] [/uri:NAME]\n\n" +
			"xsd.exe <schema>.xsd /dataset [/element:NAME] [/language:NAME]\n" +
			"            [/namespace:NAME] [/outputdir:PATH] [/uri:NAME]\n\n" +

			"xsd.exe <assembly>.dll|<assembly>.exe [/outputdir:PATH] [/type:NAME]\n\n" +
			"xsd.exe <instance>.xml [<instance>.xml ...] [/outputdir:PATH]\n\n" +
			"   /c /classes        Generate classes for the specified schema.\n" +
			"   /d /dataset        Generate typed dataset classes for the specified schema.\n" +
			"   /e /element:NAME   Element from schema to generate code for.\n" +
			"                      Multiple elements can be specified.\n" +
			"   /u /uri:NAME       Namespace uri of the elements to generate code for.\n" +
			"   /l /language:NAME  The language, or type name of custom CodeDomProvider\n" +
			"                      to use for the generated code.\n" +
			"                      Shorthand specifiers are: \"CS\" (C#) and \"VB\" (VB.NET).\n" +
			"                      For type name, assembly qualified name is required.\n" +
			"   /g /generator:TYPE Code Generator type name, followed by ','\n" + 
			"                      and assembly file name.\n" +
			"   /o /outputdir:PATH The directory where to generate the code or schemas.\n" +
			"   /n /namespace:NAME Namespace for the generated code.\n" +
			"   /t /type:NAME      Type for which to generate an xml schema.\n" +
			"                      Multiple types can be specified.\n" +
			"   /h /help           Output this help.\n";

		static readonly string incorrectOrder = "Options must be specified after Assembly or schema file names";
		static readonly string duplicatedParam = "The option {0} cannot be specified more that once";
		static readonly string unknownOption = "Unknown option {0}";
		static readonly string incompatibleArgs = "Cannot mix options for generating schemas and for generatic classes";
		static readonly string invalidParams = "Invalid parameters";
		static readonly string tooManyAssem = "Only one assembly name can be specified";
		static readonly string errLoadAssembly = "Could not load assembly: {0}";
		static readonly string typeNotFound = "Type {0} not found in the specified assembly";
		static readonly string languageNotSupported = "The language {0} is not supported";
		static readonly string missingOutputForXsdInput = "Can only generate one of classes or datasets.";
		static readonly string generatorAssemblyNotFound = "Could not load code provider assembly file: {0}";
		static readonly string generatorTypeNotFound = "Could not find specified code provider type: {0}";
		static readonly string generatorTypeIsNotCodeGenerator = "Specified code provider type was not CodeDomProvider: {0}";
		static readonly string generatorThrewException = "Specified CodeDomProvider raised an error while creating its instance: {0}";

		static void Main (string [] args)
		{
			if (args.Length < 1) {
				Console.WriteLine (helpString);
				Environment.Exit (0);
			}

			try
			{
				new Driver().Run (args);
			}
			catch (ApplicationException ex)
			{
				Console.WriteLine (ex.Message);
			}
			catch (Exception ex)
			{
				Console.WriteLine (ex);
			}
		}

		string outputDir = null;


		ArrayList lookupTypes = new ArrayList();
		ArrayList assemblies = new ArrayList();

		ArrayList schemaNames = new ArrayList();
		ArrayList inferenceNames = new ArrayList();
		ArrayList elements = new ArrayList();
		string language = null;
		string namesp = null;
		string uri = null;
		string providerOption = null;
		CodeDomProvider provider;

		public void Run (string[] args)
		{
			ArrayList unknownFiles = new ArrayList();
			bool generateClasses = false;
			bool readingFiles = true;
			bool schemasOptions = false;
			bool assemblyOptions = false;
			bool generateDataset = false;
			bool inference = false;

			foreach (string arg in args)
			{
				if (!arg.StartsWith ("--") && !arg.StartsWith ("/") ||
					(arg.StartsWith ("/") && arg.IndexOfAny (Path.InvalidPathChars) == -1)
					) 
				{
					if ((arg.EndsWith (".dll") || arg.EndsWith (".exe")) && !arg.Substring (1).StartsWith ("generator:") && !arg.Substring (1).StartsWith ("g:"))
					{
						if (!readingFiles) throw new ApplicationException (incorrectOrder);
						assemblies.Add (arg);
						assemblyOptions = true;
						continue;
					}
					else if (arg.EndsWith (".xsd"))
					{
						if (!readingFiles) Error (incorrectOrder);
						schemaNames.Add (arg);
						schemasOptions = true;
						continue;
					}
					else if (arg.EndsWith (".xml"))
					{
						if (generateClasses || generateDataset) Error (duplicatedParam);
						inferenceNames.Add (arg);
						inference = true;
						continue;
					}
					else if (!arg.StartsWith ("/"))
					{
						if (!readingFiles) Error (incorrectOrder);
						unknownFiles.Add (arg);
						continue;
					}
				}

				readingFiles = false;

				int i = arg.IndexOf (":");
				if (i == -1) i = arg.Length;
				string option = arg.Substring (1,i-1);
				string param = (i<arg.Length-1) ? arg.Substring (i+1) : "";

				if (option == "classes" || option == "c")
				{
					if (generateClasses || generateDataset || inference) Error (duplicatedParam, option);
					generateClasses = true;
					schemasOptions = true;
				}
				else if (option == "dataset" || option == "d")
				{
					if (generateClasses || generateDataset || inference) Error (duplicatedParam, option);
					generateDataset = true;
					schemasOptions = true;
				}
				else if (option == "element" || option == "e")
				{
					elements.Add (param);
					schemasOptions = true;
				}
				else if (option == "language" || option == "l")
				{
					if (provider != null) Error (duplicatedParam, option);
					if (language != null) Error (duplicatedParam, option);
					language = param;
					schemasOptions = true;
				}
				else if (option == "namespace" || option == "n")
				{
					if (namesp != null) Error (duplicatedParam, option);
					namesp = param;
					schemasOptions = true;
				}
				else if (option == "outputdir" || option == "o")
				{
					if (outputDir != null) Error (duplicatedParam, option);
					outputDir = param;
				}
				else if (option == "uri" || option == "u")
				{
					if (uri != null) Error (duplicatedParam, option);
					uri = param;
					schemasOptions = true;
				}
				else if (option == "type" || option == "t")
				{
					lookupTypes.Add (param);
					assemblyOptions = true;
				}
				else if (option == "generator" || option == "g")
				{
					providerOption = param;
				}
				else if (option == "help" || option == "h")
				{
					Console.WriteLine (helpString);
					return;
				}
				else if (option == "nologo")
				{
					// ignore, since we do not output a logo anyway
				}
				else
					Error (unknownOption, option);
			}

			if (!schemasOptions && !assemblyOptions && !inference)
				Error (invalidParams);

			if (schemasOptions && assemblyOptions)
				Error (incompatibleArgs);

			if (assemblies.Count > 1)
				Error (tooManyAssem);

			if (outputDir == null) outputDir = ".";

			string typename = null;
			Type generatorType = null;

			if (language != null) {
				switch (language) {
				case "CS":
					provider = new CSharpCodeProvider ();
					break;
				case "VB":
					provider = new VBCodeProvider ();
					break;
				default:
					typename = StripQuot (language);

					generatorType = Type.GetType (typename);
					if (generatorType == null)
						Error (generatorTypeNotFound, typename);
					break;
				}
			}

			if (providerOption != null) {
				string param = providerOption;
				int comma = param.IndexOf (',');
				if (comma < 0) {
					typename = StripQuot (param);
					generatorType = Type.GetType (param);
				} else {
					typename = param.Substring (0, comma);
					string asmName = param.Substring (comma + 1);
#if NET_1_1
					Assembly asm = Assembly.LoadFile (asmName);
#else
					Assembly asm = Assembly.LoadFrom (asmName);
#endif
					if (asm == null)
						Error (generatorAssemblyNotFound, asmName);
					generatorType = asm.GetType (typename);
				}
				if (generatorType == null)
					Error (generatorTypeNotFound, typename);
			}
			if (generatorType != null) {
				if (!generatorType.IsSubclassOf (typeof (CodeDomProvider)))
					Error (generatorTypeIsNotCodeGenerator, typename);
				try {
					provider = (CodeDomProvider) Activator.CreateInstance (generatorType, null);
				} catch (Exception ex) {
					Error (generatorThrewException, generatorType.AssemblyQualifiedName.ToString () + " --> " + ex.Message);
				}
				Console.WriteLine ("Loaded custom generator type " + generatorType + " .");
			}
			if (provider == null)
				provider = new CSharpCodeProvider ();

			if (schemasOptions)
			{
				if (!generateClasses && !generateDataset)
					Error (missingOutputForXsdInput);
				schemaNames.AddRange (unknownFiles);
				if (generateClasses)
					GenerateClasses ();
				else if (generateDataset)
					GenerateDataset ();
			}
			else if (inference)
			{
				foreach (string xmlfile in inferenceNames) {
					string genFile = Path.Combine (outputDir, Path.GetFileNameWithoutExtension (xmlfile) + ".xsd");
					DataSet ds = new DataSet ();
					ds.InferXmlSchema (xmlfile, null);
					ds.WriteXmlSchema (genFile);
					Console.WriteLine ("Written file " + genFile);
				}
			}
			else
			{
				assemblies.AddRange (unknownFiles);
				GenerateSchemas ();
			}
		}

		public void GenerateSchemas ()
		{
			Assembly assembly = null;
			try
			{
				assembly = Assembly.LoadFrom ((string) assemblies [0]);
			}
			catch (Exception ex)
			{
				Error (errLoadAssembly, ex.Message);
			}
			
			Type[] types;
			
			if (lookupTypes.Count > 0)
			{
				types = new Type [lookupTypes.Count];
				for (int n=0; n<lookupTypes.Count; n++)
				{
					Type t = assembly.GetType ((string)lookupTypes[n]);
					if (t == null) Error (typeNotFound, (string)lookupTypes[n]);
					types[n] = t;
				}
			}
			else
				types = assembly.GetExportedTypes ();

			XmlReflectionImporter ri = new XmlReflectionImporter ();
			XmlSchemas schemas = new XmlSchemas ();
			XmlSchemaExporter sx = new XmlSchemaExporter (schemas);

			foreach (Type type in types)
			{
				XmlTypeMapping tm = ri.ImportTypeMapping (type);
				sx.ExportTypeMapping (tm);
			}

			if (schemas.Count == 1)
			{
				string fileName = Path.Combine (outputDir, "schema.xsd");
				WriteSchema (fileName, schemas [0]);
			}
			else
			{
				for (int n=0; n<schemas.Count; n++)
				{
					string fileName = Path.Combine (outputDir, "schema" + n + ".xsd");
					WriteSchema (fileName, schemas [n]);
				}
			}
		}

		void WriteSchema (string fileName, XmlSchema schema)
		{
			StreamWriter sw = new StreamWriter (fileName);
			schema.Write (sw);
			sw.Close ();
			Console.WriteLine ("Written file " + fileName);
		}
		
		public void GenerateClasses ()
		{
			if (namesp == null) namesp = "Schemas";
			if (uri == null) uri = "";
			string targetFile = "";

			XmlSchemas schemas = new XmlSchemas();
			foreach (string fileName in schemaNames)
			{
				StreamReader sr = new StreamReader (fileName);
				schemas.Add (XmlSchema.Read (sr, new ValidationEventHandler (HandleValidationError)));
				sr.Close ();

				if (targetFile == "") targetFile = Path.GetFileNameWithoutExtension (fileName);
				else targetFile += "_" + Path.GetFileNameWithoutExtension (fileName);
			}

			targetFile += "." + provider.FileExtension;

			CodeCompileUnit cunit = new CodeCompileUnit ();
			CodeNamespace codeNamespace = new CodeNamespace (namesp);
			cunit.Namespaces.Add (codeNamespace);
			codeNamespace.Comments.Add (new CodeCommentStatement ("\nThis source code was auto-generated by MonoXSD\n"));

			// Locate elements to generate

			ArrayList qnames = new ArrayList ();
			if (elements.Count > 0)
			{
				foreach (string name in elements)
					qnames.Add (new XmlQualifiedName (name, uri));
			}
			else
			{
				foreach (XmlSchema schema in schemas) {
					if (!schema.IsCompiled) schema.Compile (new ValidationEventHandler (HandleValidationError));
					foreach (XmlSchemaElement el in schema.Elements.Values)
						if (!qnames.Contains (el.QualifiedName))
							qnames.Add (el.QualifiedName);
				}
			}

			// Import schemas and generate the class model

			XmlSchemaImporter importer = new XmlSchemaImporter (schemas);
			XmlCodeExporter sx = new XmlCodeExporter (codeNamespace, cunit);

			ArrayList maps = new ArrayList();

			foreach (XmlQualifiedName qname in qnames)
			{
				XmlTypeMapping tm = importer.ImportTypeMapping (qname);
				if (tm != null) maps.Add (tm);
			}
			
			foreach (XmlTypeMapping tm in maps)
			{
				sx.ExportTypeMapping (tm);
			}

			// Generate the code
			
			ICodeGenerator gen = provider.CreateGenerator();

			string genFile = Path.Combine (outputDir, targetFile);
			StreamWriter sw = new StreamWriter(genFile, false);
			gen.GenerateCodeFromCompileUnit (cunit, sw, new CodeGeneratorOptions());
			sw.Close();

			Console.WriteLine ("Written file " + genFile);
		}

		public void GenerateDataset ()
		{
			if (namesp == null) namesp = "";
			if (uri == null) uri = "";
			string targetFile = "";

			DataSet dataset = new DataSet ();
			foreach (string fileName in schemaNames)
			{
				dataset.ReadXmlSchema (fileName);

				if (targetFile == "") targetFile = Path.GetFileNameWithoutExtension (fileName);
				else targetFile += "_" + Path.GetFileNameWithoutExtension (fileName);
			}

			targetFile += "." + provider.FileExtension;

			CodeCompileUnit cunit = new CodeCompileUnit ();
			CodeNamespace codeNamespace = new CodeNamespace (namesp);
			cunit.Namespaces.Add (codeNamespace);
			codeNamespace.Comments.Add (new CodeCommentStatement ("\nThis source code was auto-generated by MonoXSD\n"));

			// Generate the code
			
			ICodeGenerator gen = provider.CreateGenerator ();

			TypedDataSetGenerator.Generate (dataset, codeNamespace, gen);

			string genFile = Path.Combine (outputDir, targetFile);
			StreamWriter sw = new StreamWriter(genFile, false);
			gen.GenerateCodeFromCompileUnit (cunit, sw, new CodeGeneratorOptions());
			sw.Close();

			Console.WriteLine ("Written file " + genFile);
		}

		void HandleValidationError (object o, ValidationEventArgs e)
		{
			Console.WriteLine ("{0}: {1} {2}",
				e.Severity == XmlSeverityType.Error ? "Error" : "Warning",
				e.Message,
				e.Exception != null ? e.Exception.Message : null);
		}

		public void Error (string msg)
		{
			throw new ApplicationException (msg);
		}

		public void Error (string msg, string param)
		{
			throw new ApplicationException (string.Format(msg,param));
		}

		private string StripQuot (string input)
		{
			if (input.Length < 2)
				return input;
			if (input [0] == '"' && input [input.Length -1] == '"' ||
				input [0] == '\'' && input [input.Length - 1] == '\'')
				return input.Substring (1, input.Length - 2);
			else
				return language;
		}
	}
}
