//===============================================================================
// Microsoft patterns & practices
// Web Service Software Factory
//===============================================================================
// Copyright  Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===============================================================================
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious.  No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
//===============================================================================

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.IO;
using System.CodeDom;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using System.Xml.Serialization.Advanced;
using System.Data;
using System.Diagnostics;

namespace Microsoft.Practices.SoftwareFactories.ServiceFactory.Actions.CodeDom
{
    /// <summary>
    /// Generate classes files from XSD schemas
    /// </summary>
    class SchemaTypeGenerator
    {
        private string targetNamespace;

        /// <summary>
        /// Initializes the type generator
        /// </summary>
        /// <param name="targetNamespace">Namespace for the generated classes</param>
        public SchemaTypeGenerator(string targetNamespace)
        {
            this.targetNamespace = targetNamespace;
        }

        /// <summary>
        /// Generates the types for the schema files
        /// </summary>
        /// <param name="items">Schema files</param>
        public void GenerateTypes(IFileSource[] items)
        {
            List<IFileSource> schemaSources = new List<IFileSource>(items);
            IList<SchemaItem> schemas = ReadSchemas(schemaSources);
            GenerateTypes(schemas);
        }

        /// <summary>
        /// Read the schemas resolving dependencies
        /// </summary>
        /// <param name="schemaSources">Schema files</param>
        /// <returns>Read schema files</returns>
        private static IList<SchemaItem> ReadSchemas(List<IFileSource> schemaSources)
        {
            List<SchemaItem> schemas = new List<SchemaItem>();

            for (int i = 0; i < schemaSources.Count; i++)
            {
                IFileSource item = schemaSources[i];

                using (XmlReader schemaReader = XmlReader.Create(item.FullPath))
                {
                    bool skipSchema = false;

                    XmlSchema xsd = XmlSchema.Read(schemaReader, delegate(object sender, ValidationEventArgs e)
                    {
                        if (e.Severity == XmlSeverityType.Error)
                        {
                            // Stop processing the offending schema.
                            skipSchema = true;
                        }
                    });

                    // Skip dataset schema types.
                    if (!skipSchema)
                    {
                        skipSchema = IsDataSetSchema(item.FullPath);
                    }

                    if (!skipSchema)
                    {
                        schemas.Add(new SchemaItem(item, xsd));

                        // Add included schemas not yet processed
                        foreach (XmlSchemaObject include in xsd.Includes)
                        {
                            IFileSource includeSource = item.GetRelativeFile(((XmlSchemaExternal)include).SchemaLocation);
                            if (includeSource != null && !schemaSources.Contains(includeSource))
                                schemaSources.Add(includeSource);
                        }
                    }
                }
            }

            return schemas;
        }

        private static bool IsDataSetSchema(string xsdPath)
        {
            using (XmlReader schemaReader = XmlReader.Create(xsdPath))
            {
                schemaReader.MoveToContent();

                while (schemaReader.Read())
                {
                    if (schemaReader.NodeType == XmlNodeType.Element)
                    {
                        string isDataSetAttributeValue = schemaReader.GetAttribute("msdata:IsDataSet");
                        if (!string.IsNullOrEmpty(isDataSetAttributeValue)
                            && isDataSetAttributeValue.Equals(Boolean.TrueString, StringComparison.InvariantCultureIgnoreCase))
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Implementation of a importer extension that
        /// filters types defined in other schemas
        /// </summary>
        class ImportedTypesFilterExtension : SchemaImporterExtension
        {
            private string schemaUri;

            public ImportedTypesFilterExtension(string schemaUri)
            {
                this.schemaUri = schemaUri;
            }

            public override string ImportSchemaType(string name, string ns, XmlSchemaObject context, XmlSchemas schemas, XmlSchemaImporter importer, CodeCompileUnit compileUnit, CodeNamespace mainNamespace, CodeGenerationOptions options, CodeDomProvider codeProvider)
            {
                XmlSchemaElement contextElement = context as XmlSchemaElement;
                if (contextElement != null)
                {
                    if (contextElement.ElementSchemaType is XmlSchemaComplexType &&
                        contextElement.ElementSchemaType.SourceUri != schemaUri)
                    {
                        XmlSchemaComplexType xmlSchemaComplexType = schemas.Find(new XmlQualifiedName(name, ns), typeof(XmlSchemaComplexType)) as XmlSchemaComplexType;

                        if (xmlSchemaComplexType != null)
                        {
                            return xmlSchemaComplexType.Name;
                        }
                        else
                        {
                            return contextElement.Name;
                        }
                    }
                }

                return base.ImportSchemaType(name, ns, context, schemas, importer, compileUnit, mainNamespace, options, codeProvider);
            }
        }

        /// <summary>
        /// Generates the type files
        /// </summary>
        /// <param name="items"></param>
        private void GenerateTypes(IEnumerable<SchemaItem> items)
        {
            XmlSchemas schemas = new XmlSchemas();
            foreach (SchemaItem item in items)
                schemas.Add(item.Schema);

            bool skipGeneration = false;

            schemas.Compile(delegate(object sender, ValidationEventArgs args)
            {
                if (args.Severity == XmlSeverityType.Error)
                {
                    Trace.TraceError(args.Message);
                    skipGeneration = true;
                }
            }, true);

            if (skipGeneration)
                return;

            foreach (SchemaItem item in items)
            {
                CodeNamespace ns = new CodeNamespace(targetNamespace);
                CodeCompileUnit unit = new CodeCompileUnit();
                unit.Namespaces.Add(ns);

                XmlSchemaImporter importer = new XmlSchemaImporter(schemas,
                    CodeGenerationOptions.GenerateProperties | CodeGenerationOptions.GenerateNewAsync,
                    new ImportContext(new CodeIdentifiers(), true));
                XmlCodeExporter exporter = new XmlCodeExporter(ns, unit,
                    CodeGenerationOptions.GenerateNewAsync | CodeGenerationOptions.GenerateProperties);
                importer.Extensions.Add(new ImportedTypesFilterExtension(item.Schema.SourceUri));

                List<XmlTypeMapping> mappings = new List<XmlTypeMapping>();

                foreach (XmlSchemaType type in item.Schema.SchemaTypes.Values)
                {
                    mappings.Add(importer.ImportSchemaType(type.QualifiedName));
                }

                foreach (XmlSchemaElement element in item.Schema.Elements.Values)
                {
                    mappings.Add(importer.ImportTypeMapping(element.QualifiedName));
                }

                foreach (XmlTypeMapping mapping in mappings)
                {
                    exporter.ExportTypeMapping(mapping);
                }

                CodeGenerator.ValidateIdentifiers(ns);

                string schemaCodeFileName = Path.GetFileNameWithoutExtension(item.Source.FullPath) + ".cs";
                IFileSource schemaCodeFile = item.Source.AddRelatedFile(schemaCodeFileName);

                CodeDomProvider provider = new CSharpCodeProvider();
                using (TextWriter writer = File.CreateText(schemaCodeFile.FullPath))
                {
                    provider.GenerateCodeFromNamespace(ns, writer, new CodeGeneratorOptions());
                }
            }
        }


        #region SchemaItem class

        private class SchemaItem
        {
            private IFileSource source;
            private XmlSchema schema;

            public SchemaItem(IFileSource source, XmlSchema schema)
            {
                this.source = source;
                this.schema = schema;
            }

            public IFileSource Source
            {
                get { return source; }
            }

            public XmlSchema Schema
            {
                get { return schema; }
            }
        }

        #endregion
    }

    /// <summary>
    /// This interface abstracts the handling of the files
    /// </summary>
    interface IFileSource
    {
        /// <summary>
        /// Full path of the file
        /// </summary>
        string FullPath { get; }

        /// <summary>
        /// Searches for another file with a relative path
        /// </summary>
        /// <param name="path"></param>
        /// <returns>The file source if it's found, null otherwise</returns>
        IFileSource GetRelativeFile(string path);

        /// <summary>
        /// Adds a related file
        /// </summary>
        /// <param name="name">Relative path of the file</param>
        /// <returns>The new file source</returns>
        IFileSource AddRelatedFile(string name);
    }

    /// <summary>
    /// Implementation of <see cref="IFileSource"/> to interact with <see cref="EnvDTE.ProjectItem"/>
    /// </summary>
    class ProjectItemFileSource : IFileSource
    {
        private EnvDTE.ProjectItem item;

        /// <summary>
        /// Initializes a new instance associated with a ProjectItem
        /// </summary>
        /// <param name="item"></param>
        public ProjectItemFileSource(EnvDTE.ProjectItem item)
        {
            this.item = item;
        }

        #region IFileSource Members

        public string FullPath
        {
            get { return item.Properties.Item("FullPath").Value.ToString(); }
        }

        public IFileSource GetRelativeFile(string path)
        {
            if (!Path.IsPathRooted(path))
            {
                path = Path.Combine(Path.GetDirectoryName(FullPath), path);
            }

            path = path.Replace(
                Path.GetDirectoryName(
                item.DTE.Solution.Properties.Item("Path").Value.ToString()),
                "");

            EnvDTE.ProjectItem relativeItem = Microsoft.Practices.RecipeFramework.Library.DteHelper.FindItemByPath(item.DTE.Solution, path);
            if (relativeItem != null)
                return new ProjectItemFileSource(relativeItem);
            return null;
        }

        public IFileSource AddRelatedFile(string name)
        {
            string relatedFilePath = Path.Combine(Path.GetDirectoryName(FullPath), name);
            if (!File.Exists(relatedFilePath))
            {
                using (File.Create(relatedFilePath))
                {
                }
            }
            EnvDTE.ProjectItem relatedItem = item.ProjectItems.AddFromFile(relatedFilePath);
            return new ProjectItemFileSource(relatedItem);
        }

        #endregion

        #region Object method overrides

        public override bool Equals(object obj)
        {
            IFileSource other = obj as IFileSource;
            if (other != null)
            {
                return string.Equals(FullPath, other.FullPath, StringComparison.InvariantCultureIgnoreCase);
            }

            return base.Equals(obj);
        }

        public override int GetHashCode()
        {
            return FullPath.GetHashCode();
        }

        #endregion
    }
}
