#region License

/*
 * Copyright © 2002-2005 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#endregion

#region Imports

using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Reflection;
using System.Xml;
using System.Xml.Schema;
using Spring.Collections;
using Spring.Core;
using Spring.Core.IO;
using Spring.Core.TypeResolution;
using Spring.Objects.Factory.Config;
using Spring.Util;
using Spring.Validation;

#endregion

namespace Spring.Objects.Factory.Xml
{
    /// <summary>
    /// Provides a resolution mechanism for configuration parsers.
    /// </summary>
    /// <remarks>
    /// <p>
    /// The <see cref="DefaultObjectDefinitionDocumentReader"/> uses this registry
    /// class to find the parser handling a specific namespace.
    /// </p>
    /// </remarks>
    /// <author>Aleksandar Seovic</author>
    public class NamespaceParserRegistry
    {
        /// <summary>
        /// Resolves xml entities by using the <see cref="IResourceLoader"/> infrastructure.
        /// </summary>
        private class XmlResourceUrlResolver : XmlUrlResolver
        {
            public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn)
            {
                IResourceLoader resourceLoader = new ConfigurableResourceLoader();
                IResource resource = resourceLoader.GetResource(absoluteUri.AbsoluteUri);
                return resource.InputStream;
                //return base.GetEntity( absoluteUri, role, ofObjectToReturn );
            }

            public override Uri ResolveUri(Uri baseUri, string relativeUri)
            {
                // TODO: resolve Uri using IResource instance
                return base.ResolveUri(baseUri, relativeUri);
            }
        }

        /// <summary>
        /// Name of the .Net config section that contains definitions 
        /// for custom config parsers.
        /// </summary>
        private const string ConfigParsersSectionName = "spring/parsers";

        #region Fields

        private static IDictionary parsers;
        private readonly static IDictionary wellknownNamespaceParserTypeNames;

#if !NET_2_0
        private static XmlSchemaCollection schemas;
#else
        private static XmlSchemaSet schemas;
#endif

        #endregion

        /// <summary>
        /// Creates a new instance of the NamespaceParserRegistry class.
        /// </summary>
        static NamespaceParserRegistry()
        {
            wellknownNamespaceParserTypeNames = new CaseInsensitiveHashtable();
            wellknownNamespaceParserTypeNames["http://www.springframework.net/tx"] = "Spring.Transaction.Config.TxNamespaceParser, Spring.Data";
            wellknownNamespaceParserTypeNames["http://www.springframework.net/aop"] = "Spring.Aop.Config.AopNamespaceParser, Spring.Aop";
            wellknownNamespaceParserTypeNames["http://www.springframework.net/db"] = "Spring.Data.Config.DatabaseNamespaceParser, Spring.Data";
            wellknownNamespaceParserTypeNames["http://www.springframework.net/remoting"] = "Spring.Remoting.Config.RemotingNamespaceParser, Spring.Services";
            wellknownNamespaceParserTypeNames["http://www.springframework.net/nms"] = "Spring.Messaging.Nms.Config.NmsNamespaceParser, Spring.Messaging.Nms";
            wellknownNamespaceParserTypeNames["http://www.springframework.net/ems"] = "Spring.Messaging.Ems.Config.EmsNamespaceParser, Spring.Messaging.Ems";
            wellknownNamespaceParserTypeNames["http://www.springframework.net/validation"] = "Spring.Validation.Config.ValidationNamespaceParser, Spring.Core";
            wellknownNamespaceParserTypeNames["http://www.springframework.net/nvelocity"] = "Spring.Template.Velocity.Config.TemplateNamespaceParser, Spring.Template.Velocity";

            Reset();
        }

        /// <summary>
        /// Reset the list of registered parsers to "factory"-setting
        /// </summary>
        /// <remarks>use for unit tests only</remarks>
        public static void Reset()
        {
            parsers = new HybridDictionary();
#if !NET_2_0
            schemas = new XmlSchemaCollection();
#else
            schemas = new XmlSchemaSet();
            schemas.XmlResolver = new XmlResourceUrlResolver();
#endif

            //TODO - externalize default list of parsers.
            RegisterParser(new ObjectsNamespaceParser());
            // register custom config parsers
            ConfigurationUtils.GetSection(ConfigParsersSectionName);
        }

        /// <summary>
        /// Registers the <see cref="INamespaceParser"/> type for wellknown namespaces
        /// </summary>
        /// <returns><c>true</c> if the parser could be registered, <c>false</c> otherwise</returns>
        internal static bool RegisterWellknownNamespaceParserType(string namespaceUri)
        {
            if (parsers[namespaceUri] != null) return true;

            if (wellknownNamespaceParserTypeNames.Contains(namespaceUri))
            {
                string parserTypeName = (string)wellknownNamespaceParserTypeNames[namespaceUri];
                // assume, that all Spring.XXX assemblies have same version + public key
                // get the ", Version=x.x.x.x, Culture=neutral, PublicKeyToken=65e474d141e25e07" part of Spring.Core and append it
                string name = typeof(NamespaceParserRegistry).Assembly.GetName().Name;
                string fullname = typeof(NamespaceParserRegistry).Assembly.GetName().FullName;
                string versionCulturePublicKey = fullname.Substring(name.Length);

                parserTypeName = parserTypeName + versionCulturePublicKey;
                Type parserType = Type.GetType(parserTypeName, true);
                RegisterParser(parserType);
                return true;
            }
            return false;
        }

        /// <summary>
        /// Constructs a "assembly://..." qualified schemaLocation url using the given type
        /// to obtain the assembly name.
        /// </summary>
        public static string GetAssemblySchemaLocation(Type schemaLocationAssemblyHint, string schemaLocation)
        {
            if (schemaLocationAssemblyHint != null)
            {
                return "assembly://" + schemaLocationAssemblyHint.Assembly.FullName + schemaLocation;
            }
            return schemaLocation;
        }

        /// <summary>
        /// Returns a parser for the given namespace.
        /// </summary>
        /// <param name="namespaceURI">
        /// The namespace for which to lookup the parser implementation.
        /// </param>
        /// <returns>
        /// A parser for a given <paramref name="namespaceURI"/>, or
        /// <see langword="null"/> if no parser was found.
        /// </returns>
        public static INamespaceParser GetParser(string namespaceURI)
        {
            INamespaceParser parser = (INamespaceParser)parsers[namespaceURI];
            if (parser == null)
            {
                bool ok = RegisterWellknownNamespaceParserType(namespaceURI);
                if (ok)
                {
                    parser = (INamespaceParser)parsers[namespaceURI];
                }
            }
            return parser;
        }

        /// <summary>
        /// Returns a schema collection containing validation schemas for all registered parsers.
        /// </summary>
        /// <returns>
        /// A schema collection containing validation schemas for all registered parsers.
        /// </returns>
#if !NET_2_0
        public static XmlSchemaCollection GetSchemas()
#else
        public static XmlSchemaSet GetSchemas()
#endif
        {
            return schemas;
        }

        /// <summary>
        /// Pegisters parser, using default namespace and schema location
        /// as defined by the <see cref="NamespaceParserAttribute"/>.
        /// </summary>
        /// <param name="parserType">
        /// The <see cref="System.Type"/> of the parser that will be activated
        /// when an element in its default namespace is encountered.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// If <paramref name="parserType"/> is <see langword="null"/>.
        /// </exception>
        public static void RegisterParser(Type parserType)
        {
            RegisterParser(parserType, null, null);
        }

        /// <summary>
        /// Associates a parser with a namespace.
        /// </summary>
        /// <remarks>
        /// <note>
        /// Parsers registered with the same <paramref name="namespaceUri"/> as that
        /// of a parser that has previously been registered will overwrite the existing
        /// parser.
        /// </note>
        /// </remarks>
        /// <param name="parserType">
        /// The <see cref="System.Type"/> of the parser that will be activated
        /// when the attendant <paramref name="namespaceUri"/> is
        /// encountered.
        /// </param>
        /// <param name="namespaceUri">
        /// The namespace with which to associate instance of the parser.
        /// </param>
        /// <param name="schemaLocation">
        /// The location of the XML schema that should be used for validation 
        /// of the XML elements that belong to the specified namespace 
        /// (can be any valid Spring.NET resource URI).
        /// </param>
        /// <exception cref="System.ArgumentException">
        /// If the <paramref name="parserType"/> is not a <see cref="System.Type"/>
        /// that implements the <see cref="INamespaceParser"/>
        /// interface.
        /// </exception>
        /// <exception cref="System.ArgumentNullException">
        /// If <paramref name="parserType"/> is <see langword="null"/>.
        /// </exception>
        public static void RegisterParser(Type parserType, string namespaceUri, string schemaLocation)
        {
            AssertUtils.ArgumentNotNull(parserType, "parserType");

            INamespaceParser np = null;

            if ((typeof(INamespaceParser)).IsAssignableFrom(parserType))
            {
                np = (INamespaceParser)ObjectUtils.InstantiateType(parserType);
            }
            // TODO (EE): workaround to enable smooth transition between 1.x and 2.0 style namespace handling
            else if (typeof(IObjectDefinitionParser).IsAssignableFrom(parserType))
            {
                // determine and use defaults for the namespace and schema location, if necessary
                if (StringUtils.IsNullOrEmpty(namespaceUri) || StringUtils.IsNullOrEmpty(schemaLocation))
                {
                    NamespaceParserAttribute defaults = GetDefaults(parserType);
                    if (defaults == null)
                    {
                        throw new ArgumentNullException(
                            "Either default or an explicit namespace value must be specified for a configuration parser.");
                    }
                    if (StringUtils.IsNullOrEmpty(namespaceUri))
                    {
                        namespaceUri = defaults.Namespace;
                    }
                    if (StringUtils.IsNullOrEmpty(schemaLocation))
                    {
                        schemaLocation = defaults.SchemaLocation;
                        if (defaults.SchemaLocationAssemblyHint != null)
                        {
                            schemaLocation = GetAssemblySchemaLocation(defaults.SchemaLocationAssemblyHint, schemaLocation);
                        }
                    }
                }

                IObjectDefinitionParser odParser = (IObjectDefinitionParser)ObjectUtils.InstantiateType(parserType);
                np = new ObjectDefinitionParserNamespaceParser(odParser);
            }
            else
            {
                throw new ArgumentException(
                        string.Format("The [{0}] Type must implement the INamespaceParser interface.", parserType.Name)
                        , "parserType");
            }

            RegisterParser(np, namespaceUri, schemaLocation);
        }

        /// <summary>
        /// Pegisters parser, using default namespace and schema location
        /// as defined by the <see cref="NamespaceParserAttribute"/>.
        /// </summary>
        /// <param name="parser">
        /// The parser instance.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// If <paramref name="parser"/> is <see langword="null"/>.
        /// </exception>
        public static void RegisterParser(INamespaceParser parser)
        {
            RegisterParser(parser, null, null);
        }

        /// <summary>
        /// Associates a parser with a namespace.
        /// </summary>
        /// <remarks>
        /// <note>
        /// Parsers registered with the same <paramref name="namespaceUri"/> as that
        /// of a parser that has previously been registered will overwrite the existing
        /// parser.
        /// </note>
        /// </remarks>
        /// <param name="namespaceUri">
        /// The namespace with which to associate instance of the parser.
        /// </param>
        /// <param name="parser">
        /// The parser instance.
        /// </param>
        /// <param name="schemaLocation">
        /// The location of the XML schema that should be used for validation 
        /// of the XML elements that belong to the specified namespace 
        /// (can be any valid Spring.NET resource URI).
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// If <paramref name="parser"/> is <see langword="null"/>, or if 
        /// <paramref name="namespaceUri"/> is not specified and parser class
        /// does not have default value defined using <see cref="NamespaceParserAttribute"/>.
        /// </exception>
        public static void RegisterParser(INamespaceParser parser, string namespaceUri, string schemaLocation)
        {
            AssertUtils.ArgumentNotNull(parser, "parser");

            // determine and use defaults for the namespace and schema location, if necessary
            if (StringUtils.IsNullOrEmpty(namespaceUri) || StringUtils.IsNullOrEmpty(schemaLocation))
            {
                NamespaceParserAttribute defaults = GetDefaults(parser.GetType());
                if (defaults == null)
                {
                    throw new ArgumentNullException(
                        "Either default or an explicit namespace value must be specified for a configuration parser.");
                }
                if (StringUtils.IsNullOrEmpty(namespaceUri))
                {
                    namespaceUri = defaults.Namespace;
                }
                if (StringUtils.IsNullOrEmpty(schemaLocation))
                {
                    schemaLocation = defaults.SchemaLocation;
                    if (defaults.SchemaLocationAssemblyHint != null)
                    {
                        schemaLocation = GetAssemblySchemaLocation(defaults.SchemaLocationAssemblyHint, schemaLocation);
                    }
                }
            }

            // initialize the parser
            parser.Init();

            // register parser
            lock (parsers.SyncRoot)
                lock (schemas)
                {
                    parsers[namespaceUri] = parser;
                    if (StringUtils.HasText(schemaLocation) && !schemas.Contains(namespaceUri))
                    {
                        RegisterSchema(namespaceUri, schemaLocation);
                    }
                }
        }

        /// <summary>
        /// Register a schema as well-known
        /// </summary>
        /// <param name="namespaceUri"></param>
        /// <param name="schemaLocation"></param>
        private static void RegisterSchema(string namespaceUri, string schemaLocation)
        {
            IResourceLoader resourceLoader = new ConfigurableResourceLoader();
            IResource schema = resourceLoader.GetResource(schemaLocation);
            try
            {
#if NET_1_0
                XmlTextReader schemaDocument = new XmlTextReader(schemaLocation, schema.InputStream);
                schemas.Add(namespaceUri, schemaDocument);
#elif NET_1_1
                XmlTextReader schemaDocument = new XmlTextReader(schemaLocation, schema.InputStream);
                schemas.Add(namespaceUri, schemaDocument, new XmlResourceUrlResolver());
#else
                XmlTextReader schemaDocument = new XmlTextReader(schema.Uri.AbsoluteUri, schema.InputStream);
                schemas.Add(namespaceUri, schemaDocument);
#endif
            }
            catch (Exception e)
            {
                throw new ArgumentException("Could not load schema from resource = " + schema, e);
            }
        }

        /// <summary>
        /// Returns default values for the parser namespace and schema location as 
        /// defined by the <see cref="NamespaceParserAttribute"/>.
        /// </summary>
        /// <param name="parserType">
        /// A type of the parser.
        /// </param>
        /// <returns>
        /// A <see cref="NamespaceParserAttribute"/> instance containing
        /// default values for the parser namsepace and schema location
        /// </returns>
        private static NamespaceParserAttribute GetDefaults(Type parserType)
        {
            object[] attrs = parserType.GetCustomAttributes(typeof(NamespaceParserAttribute), true);
            if (attrs.Length > 0)
            {
                return (NamespaceParserAttribute)attrs[0];
            }
            return null;
        }

        #region ObjectDefinitionParserNamespaceParser Utility class

        /// <summary>
        /// Adapts the <see cref="IObjectDefinitionParser"/> interface to <see cref="INamespaceParser"/>.
        /// Only for smooth transition between 1.x and 2.0 style namespace handling, will be dropped for 2.0
        /// </summary>
        private class ObjectDefinitionParserNamespaceParser : INamespaceParser
        {
            private readonly IObjectDefinitionParser odParser;

            public ObjectDefinitionParserNamespaceParser(IObjectDefinitionParser odParser)
            {
                this.odParser = odParser;
            }

            public void Init()
            {
                // noop
            }

            public IObjectDefinition ParseElement(XmlElement element, ParserContext parserContext)
            {
                return odParser.ParseElement(element, parserContext);
            }

            public ObjectDefinitionHolder Decorate(XmlNode node, ObjectDefinitionHolder definition, ParserContext parserContext)
            {
                return null;
            }
        }

        #endregion
    }
}