#region License

/*
 * Copyright 2002-2004 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.Xml;

using Spring.Core.TypeResolution;
using Spring.Context.Support;
using Spring.Expressions;
using Spring.Objects;
using Spring.Objects.Factory;
using Spring.Objects.Factory.Config;
using Spring.Objects.Factory.Support;
using Spring.Objects.Factory.Xml;
using Spring.Threading;
using Spring.Util;

#endregion

namespace Spring.Validation.Config
{
    /// <summary>
    /// Implementation of the custom configuration parser for validator definitions.
    /// </summary>
    /// <author>Aleksandar Seovic</author>
    [
        NamespaceParser(
            Namespace = "http://www.springframework.net/validation",
            SchemaLocationAssemblyHint = typeof(ValidationNamespaceParser),
            SchemaLocation = "/Spring.Validation.Config/spring-validation-1.1.xsd")
    ]
    public sealed class ValidationNamespaceParser : ObjectsNamespaceParser
    {
        private const string ValidatorTypePrefix = "validator: ";

        [ThreadStatic]
        private int definitionCount = 0;

        static ValidationNamespaceParser()
        {
            TypeRegistry.RegisterType(ValidatorTypePrefix + "group", typeof(ValidatorGroup));
            TypeRegistry.RegisterType(ValidatorTypePrefix + "any", typeof(AnyValidatorGroup));
            TypeRegistry.RegisterType(ValidatorTypePrefix + "exclusive", typeof(ExclusiveValidatorGroup));
            TypeRegistry.RegisterType(ValidatorTypePrefix + "collection", typeof(CollectionValidator));
            TypeRegistry.RegisterType(ValidatorTypePrefix + "required", typeof(RequiredValidator));
            TypeRegistry.RegisterType(ValidatorTypePrefix + "condition", typeof(ConditionValidator));
            TypeRegistry.RegisterType(ValidatorTypePrefix + "regex", typeof(RegularExpressionValidator));
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidationNamespaceParser"/> class.
        /// </summary>
        public ValidationNamespaceParser()
        {
            // generate unique key for instance field to be stored in LogicalThreadContext
            string FIELDPREFIX = typeof(ValidationNamespaceParser).FullName + base.GetHashCode();
        }


        /// <summary>
        /// Parse the specified element and register any resulting
        /// IObjectDefinitions with the IObjectDefinitionRegistry that is
        /// embedded in the supplied ParserContext.
        /// </summary>
        /// <param name="element">The element to be parsed into one or more IObjectDefinitions</param>
        /// <param name="parserContext">The object encapsulating the current state of the parsing
        /// process.</param>
        /// <returns>
        /// The primary IObjectDefinition (can be null as explained above)
        /// </returns>
        /// <remarks>
        /// Implementations should return the primary IObjectDefinition
        /// that results from the parse phase if they wish to used nested
        /// inside (for example) a <code>&lt;property&gt;</code> tag.
        /// <para>Implementations may return null if they will not
        /// be used in a nested scenario.
        /// </para>
        /// </remarks>
        public override IObjectDefinition ParseElement(XmlElement element, ParserContext parserContext)
        {
            if (!element.HasAttribute("id"))
            {
                throw new ObjectDefinitionStoreException(parserContext.ReaderContext.Resource, "validator", "Top-level validator element must have an 'id' attribute defined.");
            }
            this.definitionCount = 0;

            ParseAndRegisterValidator(element, parserContext);

            return null;
            //return definitionCount;
        }

        /// <summary>
        /// Parses the validator definition.
        /// </summary>
        /// <param name="id">Validator's identifier.</param>
        /// <param name="element">The element to parse.</param>
        /// <param name="parserContext">The parser helper.</param>
        /// <returns>Validator object definition.</returns>
        private IObjectDefinition ParseValidator(string id, XmlElement element, ParserContext parserContext)
        {
            string typeName = GetTypeName(element);
            string parent = GetAttributeValue(element, ObjectDefinitionConstants.ParentAttribute);

            string name = "validator: " + (StringUtils.HasText(id) ? id : this.definitionCount.ToString());

            MutablePropertyValues properties = new MutablePropertyValues();
            IConfigurableObjectDefinition od
                = parserContext.ReaderContext.ObjectDefinitionFactory.CreateObjectDefinition(
                    typeName, parent, parserContext.ReaderContext.Reader.Domain);

            od.PropertyValues = properties;
            od.IsSingleton = true;
            od.IsLazyInit = true;

            ParseAttributeIntoProperty(element, ValidatorDefinitionConstants.TestAttribute, properties, "Test");
            ParseAttributeIntoProperty(element, ValidatorDefinitionConstants.WhenAttribute, properties, "When");
            ParseAttributeIntoProperty(element, ValidatorDefinitionConstants.GroupFastValidateAttribute, properties, "FastValidate");
            ParseAttributeIntoProperty(element, ValidatorDefinitionConstants.RegexExpressionAttribute, properties, "Expression");
            ParseAttributeIntoProperty(element, ValidatorDefinitionConstants.CollectionValidateAllAttribute, properties, "ValidateAll");
            ParseAttributeIntoProperty(element, ValidatorDefinitionConstants.CollectionContextAttribute, properties, "Context");
            ParseAttributeIntoProperty(element, ValidatorDefinitionConstants.CollectionIncludeElementsErrors, properties, "IncludeElementErrors");

// TODO: (EE) - is this a mistake to check 'validateAll' but add 'context' then?
//            if (StringUtils.HasText(validateAll))
//            {
//                properties.Add("Context", context);
//            }

            ManagedList nestedValidators = new ManagedList();
            ManagedList actions = new ManagedList();
            ParserContext childParserContext = new ParserContext(parserContext.ParserHelper, od);
            foreach (XmlNode node in element.ChildNodes)
            {
                XmlElement child = node as XmlElement;
                if (child != null)
                {
                    switch (child.LocalName)
                    {
                        case ValidatorDefinitionConstants.PropertyElement:
                            string propertyName = GetAttributeValue(child, ValidatorDefinitionConstants.PropertyNameAttribute);
                            properties.Add(propertyName, base.ParsePropertyValue(child, name, childParserContext));
                            break;
                        case ValidatorDefinitionConstants.MessageElement:
                            actions.Add(ParseErrorMessageAction(child, childParserContext));
                            break;
                        case ValidatorDefinitionConstants.ActionElement:
                            actions.Add(ParseGenericAction(child, childParserContext));
                            break;
                        case ValidatorDefinitionConstants.ReferenceElement:
                            nestedValidators.Add(ParseValidatorReference(child, childParserContext));
                            break;
                        default:
                            nestedValidators.Add(ParseAndRegisterValidator(child, childParserContext));
                            break;
                    }
                }
            }
            if (nestedValidators.Count > 0)
            {
                properties.Add("Validators", nestedValidators);
            }
            if (actions.Count > 0)
            {
                properties.Add("Actions", actions);
            }

            return od;
        }

        /// <summary>
        /// Parses the attribute of the given <paramref name="attName"/> from the XmlElement and, if available, adds a property of the given <paramref name="propName"/> with
        /// the parsed value.
        /// </summary>
        private void ParseAttributeIntoProperty(XmlElement element, string attName, MutablePropertyValues properties, string propName)
        {
            string test = GetAttributeValue(element, attName);
            if (StringUtils.HasText(test))
            {
                properties.Add(propName, test);
            }
        }

        /// <summary>
        /// Parses and potentially registers a validator.
        /// </summary>
        /// <remarks>
        /// Only validators that have <code>id</code> attribute specified are registered 
        /// as separate object definitions within application context.
        /// </remarks>
        /// <param name="element">Validator XML element.</param>
        /// <param name="parserContext">The parser helper.</param>
        /// <returns>Validator object definition.</returns>
        private IObjectDefinition ParseAndRegisterValidator(XmlElement element, ParserContext parserContext)
        {
            string id = GetAttributeValue(element, ObjectDefinitionConstants.IdAttribute);
            IObjectDefinition validator = ParseValidator(id, element, parserContext);
            if (StringUtils.HasText(id))
            {
                parserContext.ReaderContext.Registry.RegisterObjectDefinition(id, validator);
                this.definitionCount++;
            }
            return validator;
        }

        /// <summary>
        /// Gets the name of the object type for the specified element.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <returns>The name of the object type.</returns>
        private string GetTypeName(XmlElement element)
        {
            string typeName = GetAttributeValue(element, ObjectDefinitionConstants.TypeAttribute);
            if (StringUtils.IsNullOrEmpty(typeName))
            {
                return ValidatorTypePrefix + element.LocalName;
            }
            return typeName;
        }

        /// <summary>
        /// Creates an error message action based on the specified message element.
        /// </summary>
        /// <param name="message">The message element.</param>
        /// <param name="parserContext">The parser helper.</param>
        /// <returns>The error message action definition.</returns>
        private static IObjectDefinition ParseErrorMessageAction(XmlElement message, ParserContext parserContext)
        {
            string messageId = GetAttributeValue(message, MessageConstants.IdAttribute);
            string[] providers = GetAttributeValue(message, MessageConstants.ProvidersAttribute).Split(',');
            ArrayList parameters = new ArrayList();

            foreach (XmlElement param in message.ChildNodes)
            {
                IExpression paramExpression = Expression.Parse(GetAttributeValue(param, MessageConstants.ParameterValueAttribute));
                parameters.Add(paramExpression);
            }

            string typeName = "Spring.Validation.Actions.ErrorMessageAction, Spring.Core";
            ConstructorArgumentValues ctorArgs = new ConstructorArgumentValues();
            ctorArgs.AddGenericArgumentValue(messageId);
            ctorArgs.AddGenericArgumentValue(providers);

            string when = GetAttributeValue(message, ValidatorDefinitionConstants.WhenAttribute);
            MutablePropertyValues properties = new MutablePropertyValues();
            if (StringUtils.HasText(when))
            {
                properties.Add("When", when);
            }
            if (parameters.Count > 0)
            {
                properties.Add("Parameters", parameters.ToArray(typeof(IExpression)));
            }

            IConfigurableObjectDefinition action =
                parserContext.ReaderContext.ObjectDefinitionFactory.CreateObjectDefinition(typeName, null, parserContext.ReaderContext.Reader.Domain);
            action.ConstructorArgumentValues = ctorArgs;
            action.PropertyValues = properties;

            return action;
        }

        /// <summary>
        /// Creates a generic action based on the specified element.
        /// </summary>
        /// <param name="element">The action definition element.</param>
        /// <param name="parserContext">The parser helper.</param>
        /// <returns>Generic validation action definition.</returns>
        private IObjectDefinition ParseGenericAction(XmlElement element, ParserContext parserContext)
        {
            string typeName = GetAttributeValue(element, ObjectDefinitionConstants.TypeAttribute);
            string when = GetAttributeValue(element, ValidatorDefinitionConstants.WhenAttribute);
            MutablePropertyValues properties = base.ParsePropertyElements("validator:action", element, parserContext);
            if (StringUtils.HasText(when))
            {
                properties.Add("When", when);
            }

            IConfigurableObjectDefinition action =
                parserContext.ReaderContext.ObjectDefinitionFactory.CreateObjectDefinition(typeName, null, parserContext.ReaderContext.Reader.Domain);
            action.PropertyValues = properties;

            return action;
        }

        /// <summary>
        /// Creates object definition for the validator reference.
        /// </summary>
        /// <param name="element">The action definition element.</param>
        /// <param name="parserContext">The parser helper.</param>
        /// <returns>Generic validation action definition.</returns>
        private IObjectDefinition ParseValidatorReference(XmlElement element, ParserContext parserContext)
        {
            string typeName = "Spring.Validation.ValidatorReference, Spring.Core";
            string name = GetAttributeValue(element, ValidatorDefinitionConstants.ReferenceNameAttribute);
            string context = GetAttributeValue(element, ValidatorDefinitionConstants.ReferenceContextAttribute);

            MutablePropertyValues properties = new MutablePropertyValues();
            properties.Add("Name", name);
            if (StringUtils.HasText(context))
            {
                properties.Add("Context", context);
            }

            IConfigurableObjectDefinition reference =
                parserContext.ReaderContext.ObjectDefinitionFactory.CreateObjectDefinition(typeName, null, parserContext.ReaderContext.Reader.Domain);
            reference.PropertyValues = properties;
            return reference;
        }

        #region Element & Attribute Name Constants

        private class ValidatorDefinitionConstants
        {
            public const string PropertyElement = "property";
            public const string MessageElement = "message";
            public const string ActionElement = "action";
            public const string ReferenceElement = "ref";

            public const string TypeAttribute = "type";
            public const string TestAttribute = "test";
            public const string NameAttribute = "name";
            public const string WhenAttribute = "when";

            public const string PropertyNameAttribute = "name";

            public const string ReferenceNameAttribute = "name";
            public const string ReferenceContextAttribute = "context";

            public const string RegexExpressionAttribute = "fast-validate";

            public const string GroupFastValidateAttribute = "fast-validate";

            public const string CollectionValidateAllAttribute = "validate-all";
            public const string CollectionContextAttribute = "context";
            public const string CollectionIncludeElementsErrors = "include-element-errors";
        }

        private class MessageConstants
        {
            public const string ParamElement = "param";

            public const string IdAttribute = "id";
            public const string ProvidersAttribute = "providers";
            public const string ParameterValueAttribute = "value";
        }

        #endregion
    }
}